Design an ATM

The state-machine showcase — card, PIN, menu, dispense — plus the question that matters: debit first or dispense first?

LLDOODstate-patternconsistency

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.

ATM sessiontime O(1) per eventspace O(states)
insertCardpinOkpinBadwithdrawdoneIdleCardAuthCash
events:insertCardpinOkwithdrawdone

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.

state = Idle

The State pattern makes each state a class owning its behavior; illegal actions become unrepresentable instead of un-checked:

Python
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

Think it through: Design an ATMLLD Classic — state + consistency0/5 stages

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. 1

    Spot the two design centers

    Before drawing classes: what are the TWO hard parts hiding in this 'easy' prompt?

  2. 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. 3

    Hide hardware behind ports

    How do I unit-test a machine with a cash drawer?

    unlocks after the stage above
  4. 4

    The ordering question

    Debit first or dispense first? Don't pick — compare the FAILURES.

    unlocks after the stage above
  5. 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):

  1. Reserve/debit at the bank — with an idempotency key (transaction id), so retrying an ambiguous timeout can't double-debit.
  2. Command the dispenser; hardware confirms notes-out (sensors).
  3. Confirm settlement to the bank.
  4. 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.

Practice ladder: State machines & transactions0/4 solved

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.
  1. Greedy note dispensing over per-denomination inventory — the cash half of the machine.

Core — guarded transactions

An action runs only when the rules hold.
  1. Withdraw / transfer that validate before they mutate — the rules behind a withdrawal.

  2. Token issue / renew / expire — the PIN-auth session as its own lifecycle.

Stretch — explicit, legal-only states

Model the screens; refuse illegal moves.
  1. A state object with only-legal forward/back moves — the finite-state discipline of the ATM's screens.