InterviewPrepKit

Home / Cheat Sheet / Object-Oriented Design

Cheat sheet

How to design an ATM

Read the full lesson →

An ATM spends cash it owns against a balance it does not: two ledgers, one irreversible motor, and a network link that can drop mid-transaction.

The three decisions

  • Model the session as an explicit state machine so the uncancellable region becomes nameable.
  • Plan notes with bounded DP, not greedy, which silently misses plans that exist.
  • Journal the intent before actuating, settle what was counted, so a jam is a reconciliation, not a dispute.

Objects and ownership

  • No actor owns two of: balance (bank), cash + record of intent (ATM), physical arbitration (servicer).
  • The ATM never decrements a balance; it is a remote actuator with a local durable log.
  • Lifted out of ATM for a reason: Session (guarded lifecycle), CashInventory (servicer mutates it), Journal (must survive power loss), BankGateway (remote, fails).
  • NotePlanner is a Strategy (interchangeable algorithm); one machine, not a Singleton.

Session state machine

  • IDLE → AUTHENTICATING → SELECTING → AUTHORIZING → DISPENSING → SETTLING → PRINTING → EJECTING
  • Uncancellable region: AUTHORIZING through SETTLING have no cancel event; machine is committed.
  • Guards: withdraw needs amount % 1000 == 0 (minor units, so multiples of $10); AUTHORIZING → DISPENSING fires journal INTENT + fsync.
  • 3 wrong PINs retains the card; pin_tries lives on Session (a UI convenience), the issuer’s counter is the real security control.
  • Jam → SUSPENDED, a state of the machine (in_service = False), outliving the session.
  • RECOVERY runs at boot, not a session state; replays unsettled journal entries.

Counting notes: bounded knapsack

  • Greedy is only correct for canonical sets (full US bill set is one); an ATM stocks a non-canonical subset.
  • Greedy fails two ways: bad denomination set ($130 from {100,50,20}), and low inventory ($80 from 1×$50, 4×$20).
  • Bounded: each denomination used at most stock[d] times; one DP layer per denomination.
  • step = gcd(amount, *stock) collapses the state space: $130 at a $10 step is 13 states, not 13,000.
  • Fewest notes ≠ operator goal: MinNotes drains one cassette (half the cash stranded in days); BalancedDrain penalises depleted cassettes.
  • Plan first, hold second. plan_notes returning None must decline while nothing is reserved or journalled.
  • Concurrency: the other writer is the servicer at the replenishment door, not a second customer. Plan and decrement in one locked step (reserve); reserve decrements at planning time.

Dispense failure: bracket the motor

Debit-first loses the customer’s money on a jam; dispense-first loses the bank’s and is repeatable (an exploit). Two non-atomic steps can’t fake atomicity by reordering.

1. Hold  (reserve, no money moves) -> auth_id
2. Journal INTENT + fsync
3. Actuate  (note counter reads `presented`)
4. Journal outcome (DISPENSED / SHORT)
5. Settle hold for `presented`, keyed by auth_id
  • settle is idempotent (keyed by auth_id); replay settles, never re-dispenses.
  • Settle the counted amount; a short dispense settles short, never more than the hold.
  • recover() at boot: for entries stuck at INTENT read disp.counted; settle is idempotent so replay is free.
  • Every crash resolves without a human except the jam (notes physically stuck in the transport).
Crash pointBankRecovery
Before holduntouchednothing happened
After hold, before INTENTheldhold ages out, auto-releases
After INTENT, before motorheldreplay settles 0, hold released
Mid-dispense (jam)heldsettle n, retract rest, out of service
After motor, before settleheldreplay settles n by auth_id
After settlecaptureddone

Gotchas

  • Money is an integer count of minor units (80_00, never 80.0); denominations are integers too, so the planner is exact. A float produces a plan that doesn’t sum, read as a phantom short dispense.
  • in_service is sticky True → False; only a servicer clears it.
  • Journal.append copies the plan dict; unsettled() walks backwards, latest state per txn_id wins.
  • Retracted notes go to a locked reject bin, not back into the cassette, so they stay a term in the servicer’s reconciliation.

Extensions

  • Deposits: same bracketed action, sign flipped (field is presented, not dispensed); adds an escrow stage and a real cancel point; credit is provisional until the servicer’s count agrees.
  • Multi-currency: extra cassette set is nearly free; the cost is a Money value object touching every signature, and capturing the FX rate in the journal at authorization time.
  • Daily limit across ATMs: shared state, must live at the issuer, checked and incremented in the same atomic step as the funds check (hold); consumed by the settled amount, released when a hold expires.
Want the full picture? The lesson has the derivations, worked examples, and diagrams this card compresses into bullets. Read the full lesson →
Report a bug