Based on the requirements and use cases, identify the main objects of the system and analyze how they interact and relate to each other...
A Customer walks up with a name, ID, and contact details. They own one or more accounts. The Customer is the human identity; accounts are the financial containers.
The customer selects their checking account. An Account holds an account number, an owner (Customer) reference, a balance, and a list of Transaction records. It provides deposit(), withdraw(), and getBalance() methods.
Why store balance as a field rather than computing it from transactions? Computing from transactions means scanning the entire history on every balance check. For an account with 10,000 transactions, that is 10,000 additions per query. Storing the balance gives O(1) reads. The trade-off is that the stored balance must always match the sum of transactions, which the lock ensures.
Each operation creates a Transaction: an immutable record with a unique ID, type (DEPOSIT, WITHDRAWAL, TRANSFER_IN, TRANSFER_OUT), amount, timestamp, resulting balance, and optional counterparty account. Immutability makes transactions thread-safe to read and tamper-proof for auditing.
A BankService orchestrates multi-account operations like transfers. It handles deadlock prevention through consistent lock ordering by account number.
A TransactionType enum (DEPOSIT, WITHDRAWAL, TRANSFER_IN, TRANSFER_OUT) categorizes each transaction for reporting and filtering.
For each class, define the attributes (data) it will hold and the methods (functions) that operate on the attributes. Ensure they align with the object's responsibilities and adhere to the principle of encapsulation. Write your code in the code editor below.
Explain design tradeoffs you considered. Check and explain whether your design adheres to SOLID principles. Explain how your design can handle changes in scale and whether it would be easy to extend with new functionalities. Identify areas for future improvement...
Single Responsibility Principle
Open/Closed Principle
Liskov Substitution Principle
Interface Segregation Principle
Dependency Inversion Principle
Think about it this way: a bank has millions of accounts, but at any given moment, most of them are idle. Locking the entire bank for one withdrawal would be like closing every highway because one intersection has a red light.
Each Account has its own ReentrantLock. Operations on different accounts run in parallel with zero contention. Only operations on the same account (or overlapping transfer pairs) contend. A bank with 1 million accounts has 1 million independent locks, enabling full parallelism.
The double-withdrawal race, made concrete:
BankService.transfer() always locks accounts in ascending account-number order. This eliminates circular wait, the necessary condition for deadlock. The ordering is stable because account numbers are unique and immutable.
Key Insight
The lock-ordering strategy in BankService.transfer() is the most important design decision. Without it, deadlocks between concurrent transfers would freeze the system. Always lock accounts in ascending account-number order.
Interview Tip
In interviews, the deadlock prevention via lock ordering is the most impressive design detail. Walk through the scenario: 'Transfer A-to-B and B-to-A both lock A first because A has the lower account number. No circular wait. No deadlock.'
Balance queries (getBalance, getTransactionHistory) are read operations. Multiple ATMs displaying the same account balance should not block each other. A ReadWriteLock allows concurrent reads while serializing writes (deposit, withdraw, transfer).
For a distributed banking system, per-account locks become distributed locks (e.g., Redis-based). The lock ordering strategy still applies: always lock accounts in ascending order across nodes. Transactions become distributed transactions using a two-phase commit or saga pattern.