ATM Machine#
Problem statement (interviewer prompt)
Design the software inside an ATM: authenticate by card + PIN, present a menu (balance / withdraw / deposit / transfer), validate balance + daily limits, dispense cash, print receipts, and handle exceptions (no cash, no network, card retained).
classDiagram
class ATM
class CardReader
class CashDispenser
class Display
class Keypad
class BankSwitch
class State
ATM --> CardReader
ATM --> CashDispenser
ATM --> Display
ATM --> Keypad
ATM --> BankSwitch
ATM --> State
classDiagram
direction TB
class ATM {
-state: ATMState
-reader: CardReader
-dispenser: CashDispenser
-depositSlot: DepositSlot
-display: Display
-keypad: Keypad
-bank: BankSwitch
-session: Session
+insertCard(c)
+enterPin(p)
+withdraw(amount)
+deposit()
+balance()
+eject()
}
class ATMState {
<<interface>>
+next(atm, input)
}
class IdleState
class HasCardState
class AuthenticatedState
class TransactionState
class OutOfServiceState
class CardReader
class CashDispenser
class DepositSlot
class Display
class Keypad
class Receipt
class BankSwitch {
+authenticate(card, pin)
+authorize(tx)
+debit(account, amount)
+credit(account, amount)
+balance(account)
}
class Transaction {
<<interface>>
+execute(atm)
}
class Withdraw
class Deposit
class Balance
class Transfer
ATM --> ATMState
ATMState <|.. IdleState
ATMState <|.. HasCardState
ATMState <|.. AuthenticatedState
ATMState <|.. TransactionState
ATMState <|.. OutOfServiceState
ATM *-- CardReader
ATM *-- CashDispenser
ATM *-- DepositSlot
ATM *-- Display
ATM *-- Keypad
ATM --> BankSwitch
ATM ..> Transaction
Transaction <|.. Withdraw
Transaction <|.. Deposit
Transaction <|.. Balance
Transaction <|.. Transfer
ATM state machine#
Most of the design effort in an ATM LLD goes into the state machine, because every screen, button press, and hardware event must be valid for exactly one current state. Modelling the lifecycle as an explicit ATMState interface (rather than a switch on an enum) is what unlocks the State pattern and keeps the ATM god-object from growing dozens of conditional branches. The diagram below shows the happy path plus the three error states an interviewer will probe: PinFailed, InsufficientFunds, and CashUnavailable.
stateDiagram-v2
direction TB
[*] --> Idle
Idle --> CardInserted: insertCard
CardInserted --> PinEntered: enterPin
CardInserted --> CardEjected: cancel
PinEntered --> MenuShown: pinValid
PinEntered --> PinFailed: pinInvalid
PinFailed --> PinEntered: retry left
PinFailed --> CardRetained: 3 strikes
MenuShown --> TransactionInProgress: selectTxn
MenuShown --> CardEjected: exit
TransactionInProgress --> DispensingCash: withdraw ok
TransactionInProgress --> InsufficientFunds: balance low
TransactionInProgress --> CashUnavailable: dispenser empty
InsufficientFunds --> MenuShown: ack
CashUnavailable --> MenuShown: ack
DispensingCash --> CardEjected: cash taken
CardEjected --> Idle: card removed
CardRetained --> Idle: ops resets
Three transitions in this diagram carry the most interview weight. First, the PinFailed -> CardRetained edge after three strikes is the security gate that justifies a retry counter on the Session object rather than on the ATM. Second, the DispensingCash -> CardEjected ordering is deliberate: the ATM must commit the debit and confirm the cash drawer fired before releasing the card, otherwise a quick-witted attacker can pull the card mid-debit. Third, CashUnavailable is a hardware-level failure that should not roll back the entire session; only the current Withdraw transaction is aborted, and the user is sent back to MenuShown so they can try a balance enquiry instead.
State pattern in Java#
The Java sketch below pins down the ATMState interface and three concrete states. The key idea is that ATM delegates every user event to state.next(this, input), and each state object decides which transition to fire and which next state to set on the context. This keeps ATM small and lets new states (for example OutOfServiceState during maintenance) be added without touching existing code.
public interface ATMState {
void onInsertCard(ATM atm, Card card);
void onEnterPin(ATM atm, String pin);
void onSelectTxn(ATM atm, Transaction t);
void onCancel(ATM atm);
}
public final class IdleState implements ATMState {
public void onInsertCard(ATM atm, Card card) {
atm.session().bindCard(card);
atm.setState(new CardInsertedState());
}
public void onEnterPin(ATM atm, String pin) { /* ignored */ }
public void onSelectTxn(ATM atm, Transaction t) { /* ignored */ }
public void onCancel(ATM atm) { /* no-op */ }
}
public final class PinEnteredState implements ATMState {
public void onEnterPin(ATM atm, String pin) {
Session s = atm.session();
if (atm.bank().authenticate(s.card(), pin)) {
atm.setState(new MenuShownState());
} else if (s.incrementFailures() >= 3) {
atm.reader().retainCard();
atm.setState(new IdleState());
}
}
/* other handlers omitted */
}
public final class DispensingCashState implements ATMState {
public void onSelectTxn(ATM atm, Transaction t) {
try {
atm.dispenser().dispense(t.amount());
atm.bank().confirmDebit(t.id());
atm.printer().print(Receipt.of(t));
atm.setState(new CardEjectedState());
} catch (HardwareException e) {
atm.bank().reverse(t.id()); // journal-based reversal
atm.setState(new CashUnavailableState());
}
}
}
public final class ATM {
private ATMState state = new IdleState();
public void setState(ATMState s) { this.state = s; }
public void insertCard(Card c) { state.onInsertCard(this, c); }
public void enterPin(String p) { state.onEnterPin(this, p); }
public void select(Transaction t) { state.onSelectTxn(this, t); }
}
Two non-obvious details: the Session (not the ATM) tracks the PIN-failure counter so resetting state after CardRetained is a single object swap, and the dispenser-failure path uses bank.reverse(t.id()) for a journal-backed reversal rather than rolling back local state. That separation is what lets the bank reconcile mid-flight crashes; see the "Edge cases" section below.
Design patterns used#
- State is the dominant pattern. Each menu, screen, and hardware mode is its own
ATMStateimplementation, and theATMcontext simply forwards events to the current state. Adding a new state (MaintenanceState,LowCashState) does not modify any existing class. - Strategy drives the cash-dispensing algorithm.
DenominationStrategy.split(amount, inventory)is typically a greedy denomination breakdown (largest notes first), but an interviewer may ask for a variant that rebalances the cassette across the day; swapping the strategy is one constructor argument. - Chain of Responsibility handles multi-step transaction validation. A
Withdrawflows throughPinCheck -> BalanceCheck -> DailyLimitCheck -> FraudCheck -> AmlCheck, each link short-circuits on failure, and new checks (geo-velocity, device-fingerprint) plug in without touching the call site. - Singleton is the right fit for the
CashDispenserhardware controller because the JVM owns exactly one physical drawer per ATM. The bank switch client and printer driver are usually singletons for the same reason. - Command wraps each
Transactionso it can be logged, retried, and reversed by id; this is what makes journal-based recovery possible.
Common variants & follow-ups#
- EMV chip + PIN vs mag-stripe swipe: the chip path runs an offline data authentication step before the PIN is even sent to the bank, which adds a
CardAuthenticatingStatebetweenCardInsertedandPinEntered. Newer terminals also support NFC tap for sub-limit amounts (typically under 5,000 INR or 100 USD) with no PIN at all. - Deposit transactions flip the dispenser for an envelope or cash-acceptor module. The acceptor counts and validates notes, so
DepositStatemust wait forCashCountedEventfrom the hardware before crediting the account; the credit is intentionally provisional until end-of-day reconciliation. - Currency-conversion ATMs at airports add a
RateProviderstrategy and quote the user in both currencies before debiting; the FX margin is recorded as a separate journal entry for compliance. - Mini-statement is a read-only transaction that hits a different RPC on the bank switch and renders the last N entries on the printer; it should never block a withdraw queue.
- Multi-account on a single card (savings + current) prompts the user to pick an account at
MenuShowntime, then carries the chosen account id through theTransactionobject so daily-limit checks scope to the right account. - Biometric authentication (fingerprint or iris) is a drop-in replacement for the PIN step. Modelled as a different
AuthenticationStrategy, it leaves the rest of the state machine untouched.
Edge cases#
- Cash dispensed but receipt printer jams: the transaction is already committed at the bank; the printer failure is an observability event, not a rollback trigger. The screen shows a graceful "receipt unavailable, transaction successful" message and the journal stores the txn id for the user to quote in support calls.
- Card eaten mid-transaction: if the card reader detects a stuck card during
TransactionInProgress, the ATM completes the in-flight transaction (cash is already counted) and then transitions toCardRetained. The retained card is logged so the branch operator can return it. - Power loss during dispense: this is why the dispenser writes a journal entry before opening the cassette door. On reboot, the ATM replays the journal, asks the dispenser for its post-fault counters, and either confirms or reverses the debit. The reversal is idempotent on the bank side, keyed by the transaction id.
- Denomination unavailable: the user asks for 6,500 INR but the cassette only has 500s; the
DenominationStrategyeither rounds down to the nearest dispensable amount and re-prompts, or refuses and routes toCashUnavailable. Both are valid; pick one and be consistent. - Daily withdrawal limit edge: the limit check must run on the bank side, not the ATM, because the user can hop between ATMs. The chain-of-responsibility step calls
bank.checkDailyLimit(account, amount)which returns the residual headroom. - Concurrent ATM access on the same account: two cards (joint account) at two ATMs in the same minute. The bank switch serialises debits through an account-level lock or an optimistic-concurrency token; the ATM treats a
ConcurrencyConflictExceptionas a transient error and prompts the user to retry.
Glossary & fundamentals#
| Concept | What it is | Fundamentals |
|---|---|---|
| State machine | ATM states | mvcc-isolation-levels |
| Idempotency | reversal-safe withdrawals | idempotency-retries |
| Distributed transactions | bank switch saga | distributed-transactions |
| Resilience patterns | dispenser failure recovery | resilience-patterns |
Quick reference#
Functional#
- Card insert + PIN.
- Withdraw, deposit, balance, transfer, mini-statement.
- Receipt print.
Non-functional#
- 99.9%+ uptime.
- Reversal handling: if cash not dispensed, reverse the debit.
Patterns#
- State, Command (transactions), Strategy (denomination split).
Trade-offs#
- Online vs offline mode: some ATMs allow limited offline with delayed posting.
- Cash inventory management with denomination optimization is its own LLD.
Refs#
- Common LLD references; banking integration whitepapers.
FAQ#
How do you design an ATM in LLD?#
Model an ATM that owns CardReader, Keypad, Display, CashDispenser, and a BankSwitch. Use the State pattern (Idle, CardInserted, Authenticated, ChoosingTxn) so each input is handled by the current state object.
Why use the State pattern in ATM design?#
Each ATM state has different valid inputs. Encapsulating behavior in state classes avoids large switch statements and makes adding states (OutOfCash, NetworkDown) safe.
How is authentication modeled?#
Card holds the card number; the AuthenticatedState validates PIN with BankSwitch within a retry counter. On success it transitions to ChoosingTxn; on lockout it ejects or retains the card.
How is the withdraw flow designed?#
Withdraw uses a Transaction object with amount, account, and timestamp. The ATM debits via BankSwitch atomically, then dispenses cash and prints a receipt. Any step failure rolls back.
How do you handle hardware exceptions like no cash?#
CashDispenser raises a domain exception (OutOfCashException). The state transitions to OutOfCashState which only allows balance inquiry and gracefully tells the user.
Related Topics#
- Vending Machine: same State pattern applied to a simpler hardware loop
- State Machines: canonical guide to modelling lifecycles in code
- Idempotency & Retries: reversal-safe debits and journal-based recovery