Scope it first
"Card-based ATM: authenticate with PIN, check balance, withdraw cash, deposit. It talks to the bank over a network. Deep-dive withdrawal — including what happens when the network dies mid-transaction. OK?"
Two design centers: the session state machine (the cleanest State- pattern showcase in the catalog) and the dispense-vs-debit ordering problem — a distributed-consistency question wearing a metal box, and the reason this "easy" classic appears in senior loops.
The state machine
An ATM session is nothing but states — every button means something different depending on where you are:
Run a session below: card → PIN → withdraw → done. Then try the attack the State
pattern exists to stop — put withdraw first, while still Idle. There's no
transition for it, so it's refused; "dispense without auth" isn't checked for,
it has no code path at all.
1/5Start in Idle. Each event is handled by the current state — the State pattern moves this branching out of one giant switch and into the state objects themselves.
The State pattern makes each state a class owning its behavior; illegal actions become unrepresentable instead of un-checked:
class AtmState(ABC):
def insert_card(self, atm, card): raise InvalidAction()
def enter_pin(self, atm, pin): raise InvalidAction()
def withdraw(self, atm, amount): raise InvalidAction() # default: refuse
class IdleState(AtmState):
def insert_card(self, atm, card):
atm.card = card
atm.state = HasCardState()
class HasCardState(AtmState):
def enter_pin(self, atm, pin):
if atm.bank.verify(atm.card, pin):
atm.state = AuthenticatedState()
else:
atm.failed += 1
if atm.failed >= 3:
atm.retain_card() # state machine + security policy
atm.state = IdleState()
Compare the alternative — one class with if self.state == "HAS_CARD" and action == "withdraw"... for every combination — and the pattern
sells itself: adding a state (maintenance mode, deposit flow) is a new
class, not 12 new elifs. Two timers ride along: every non-Idle state
has a timeout → eject → Idle edge (abandoned sessions self-heal —
the TTL philosophy), and the card is held by the
machine until the session ends for a human-factors reason
interviewers enjoy: dispense-before-eject is how people historically
left cards behind.
Hardware behind interfaces
The controller never touches metal — it talks to ports
(Adapter territory): CardReader,
CashDispenser, Screen, Keypad, BankNetwork. Tests inject
fakes; a new dispenser model is a new adapter. The one with logic
inside is the dispenser:
Denomination dispensing
"₹3,700 from notes of 2000/500/100" is the coin-change problem with inventory limits — and because real currencies are canonical, greedy works: largest note first, bounded by what's in each cassette; if the remainder can't be formed (out of 100s), fail before dispensing anything. A Chain of Responsibility of cassette handlers (2000s → 500s → 100s) models it cleanly — each link takes what it can, passes the remainder. Edge to volunteer: amounts not formable by any combination (₹3,750 with no 50s) are rejected at amount entry — validate early, fail cheap.
Think it through like the interview
PROBLEMDesign an ATM: PIN auth, balance, withdraw, deposit. It talks to the bank over a network. The interviewer will steer you into 'what if the network dies mid-withdrawal?'
- 1
Spot the two design centers
“Before drawing classes: what are the TWO hard parts hiding in this 'easy' prompt?”
- 2
Make illegal actions unrepresentable
“Where does 'you can't withdraw before entering a PIN' live in the code?”
unlocks after the stage above - 3
Hide hardware behind ports
“How do I unit-test a machine with a cash drawer?”
unlocks after the stage above - 4
The ordering question
“Debit first or dispense first? Don't pick — compare the FAILURES.”
unlocks after the stage above - 5
Survive the ambiguous timeout
“The debit request times out — did it land? The machine can't know. Now what?”
unlocks after the stage above
The real question: debit first, or dispense first?
Withdrawal touches two systems that can't share a transaction: the bank's ledger (over a network) and the physical cash drawer. Whatever order you pick, the failure between the two steps is the interview:
- Dispense → debit: machine pays out, network dies before the debit lands → free money, multiplied across a fleet. Unacceptable.
- Debit → dispense (what real ATMs do): debit succeeds, dispenser jams → customer charged, no cash. Bad — but recoverable, and that asymmetry is the whole answer: money not yet given out can be refunded by software; cash in a stranger's hand cannot be recalled.
So the protocol is debit-first plus compensation (the saga shape, in miniature):
- Reserve/debit at the bank — with an idempotency key (transaction id), so retrying an ambiguous timeout can't double-debit.
- Command the dispenser; hardware confirms notes-out (sensors).
- Confirm settlement to the bank.
- Dispense failed? Send a reversal with the same transaction id; if the network is down, queue the reversal locally and replay — the customer is made whole minutes later, and the journal (an append-only local log of every step — event sourcing in a metal box) is the evidence for disputes.
Every step writes the journal before acting — after any crash, the machine replays its journal to discover what it was doing and completes or compensates. That's a write-ahead log (the database trick), reinvented at 4 AM in a gas station.
Practice — level up
An ATM is a finite state machine wrapped around money-safe transactions: each action is legal only in the right state, and the debit has to survive a network that lies. These drills isolate each half.
Climb in order — every rung assumes the one above it. Solve on LeetCode, then tick it here; progress is saved on this device.
Warm-up — the literal machine
Dispense cash, denomination by denomination.- Design an ATM MachineMedium
Greedy note dispensing over per-denomination inventory — the cash half of the machine.
Core — guarded transactions
An action runs only when the rules hold.- Simple Bank SystemMedium
Withdraw / transfer that validate before they mutate — the rules behind a withdrawal.
Token issue / renew / expire — the PIN-auth session as its own lifecycle.
Stretch — explicit, legal-only states
Model the screens; refuse illegal moves.- Design Browser HistoryMedium
A state object with only-legal forward/back moves — the finite-state discipline of the ATM's screens.