Code a cash machine (ATM)
Open in the interactive app: practice, quiz, mock interview →In one minute
- An ATM is a state machine wrapped around a two-party transaction. The machine owns the cash; the bank owns the money. Every withdrawal is "the bank promises the funds, the machine moves paper, then both agree on what happened", and the design is judged on what it does when those steps disagree.
- Use a hold, dispense, then commit or release protocol. Ask the bank to reserve the amount, try to dispense, and only then make the debit final. If the dispenser jams after the hold, release the hold and record why; the customer is never charged for cash they did not get.
- Model the session as explicit states (idle, card inserted, authenticated, out of service) so "withdraw before the PIN" or "insert a card while one is inside" has exactly one place that refuses it.
- Plan the notes before you ask the bank. Whether the cassettes can make the amount is a local, free question; asking the bank first creates a hold you then have to undo.
- Everything that touches hardware or the network is an injected interface: bank, dispenser, card reader, clock. That is what makes jams, timeouts and midnight limit resets testable on a laptop.
Requirements
Functional:
- A customer inserts a card and enters a PIN. Three wrong PINs in one session retain the card.
- An authenticated customer can withdraw cash, check the balance and eject the card. Several withdrawals per session are allowed.
- Withdrawals are paid from cassettes of fixed denominations (for example 50s and 20s) with limited counts, using the fewest notes. An amount the cassettes cannot make exactly is refused before the bank is contacted.
- A per-card daily withdrawal limit applies, resetting at midnight by the bank's clock.
- If the dispenser fails, the customer is not charged, the event is journaled, and the machine takes itself out of service until a technician resets it.
- Every attempted withdrawal is written to an electronic journal with its outcome.
Non-functional:
- No money is created or lost: cash out of the cassettes plus cash in the reject bin always reconciles with committed transactions.
- A single physical customer at a time; the software must still be safe against the bank replying late or twice.
- A transaction completes in a few seconds; the network round trip to the bank dominates.
Out of scope: deposits with note validation, card-less withdrawals, fee calculation and multi-currency dispensing. Mention them and park them in Extensibility.
Entities and relationships
- ATM is the façade that the screen and keypad drive. It holds the current state, the cassettes, the session and the journal.
- Session lives from card insertion to ejection: the card number, the PIN attempt count and whether the customer is authenticated. It is discarded at the end, which is how attempt counts reset.
- Cassette holds notes of one denomination and a count. The CashDispenser owns the cassettes and a reject bin, and turns a note plan into physical movement.
- NotePlan is a value: denomination to count. It is computed by a DispensePlanner and passed to the dispenser.
- BankClient is the gateway to the bank's authorisation host: verify PIN, place a hold, commit, release, and read the balance. In production it speaks ISO 8583; here it is an interface.
- Transaction records one attempt: an id, the card, the amount, the plan and the outcome (committed, released, refused). The Journal is an append-only list of them.
- Clock supplies time for daily limits and journal timestamps.
Relationships: the ATM is composed of the dispenser and cassettes (they are physically inside it). It depends on the bank client and clock through interfaces. A transaction references a session's card but outlives the session in the journal.
Class diagram
classDiagram
class ATM {
-state: AtmState
-session: Session
-journal: Transaction[]
-dailyLimit: int
+insertCard(card)
+enterPin(pin) string
+withdraw(amount) NotePlan
+ejectCard() string
+service()
}
class AtmState {
<<enumeration>>
IDLE
CARD_INSERTED
AUTHENTICATED
OUT_OF_SERVICE
}
class Session {
+card: string
+pinAttempts: int
}
class CashDispenser {
-cassettes: Map~int, int~
-rejectBin: Map~int, int~
+plan(amount) NotePlan
+dispense(plan) bool
}
class BankClient {
<<interface>>
+verifyPin(card, pin) bool
+hold(txnId, card, amount) bool
+commit(txnId)
+release(txnId)
}
class Transaction {
+id: string
+card: string
+amount: int
+outcome: string
}
class Clock {
<<interface>>
+now() int
}
ATM --> AtmState
ATM "1" o-- "0..1" Session
ATM "1" *-- "1" CashDispenser
ATM "1" *-- "many" Transaction
ATM --> BankClient
ATM --> ClockKey flows
The withdrawal is the flow that matters. Note the order: local checks, then the bank hold, then the hardware, then the final message to the bank.
sequenceDiagram
participant C as Customer
participant A as ATM
participant D as CashDispenser
participant B as BankClient
C->>A: withdraw(110)
A->>A: state is AUTHENTICATED, under daily limit
A->>D: plan(110)
D-->>A: {50: 1, 20: 3}
A->>B: hold(TX7, card, 110)
B-->>A: approved
A->>D: dispense(plan)
alt notes presented
D-->>A: ok
A->>B: commit(TX7)
A->>A: journal committed, add to daily total
A-->>C: take your cash
else jam
D-->>A: fault, notes to reject bin
A->>B: release(TX7)
A->>A: journal released, OUT_OF_SERVICE
A-->>C: not charged, card returned
endDesign patterns used and why
- State for the session. Each operation checks one thing first: is it legal in the current state. With four states and five operations, an enum plus a guard at the top of each method is clear enough; promote to State objects (one class per state, as in Design Vending Machine) when states start carrying their own data or timers, such as a "waiting for cash to be taken" state with a 30-second retract.
- Command for transactions. Withdraw, balance inquiry and later deposit are objects with
executeand an outcome, so they can be journaled and recovered after a power cut. - Template Method for the transaction skeleton: validate locally, authorise with the bank, perform the physical action, complete or reverse. Withdrawal and deposit fill in the steps differently but share the order and the reversal rule.
- Façade and Adapter around hardware and the bank. The ATM talks to
CashDispenserandBankClient, not to vendor drivers or ISO 8583 bitmaps. The CEN/XFS standard applies the same idea industry-wide. - Chain of Responsibility for notes is a trap. Many textbook answers pass the amount down a chain of 50, 20 and 10 handlers, each taking as many notes as it can. That is greedy, and greedy fails: with only 50s and 20s, 110 becomes 50 + 50 with 10 left over, though 50 + 20 + 20 + 20 works. Use a small search over counts instead (the vending machine lesson covers the dynamic program).
Concurrency and edge cases
- Bank reply lost after the hold. The ATM sent the hold, the network dropped the reply, and it times out. It does not know whether funds are reserved. Send a release for that transaction id anyway; the bank must treat a release for an unknown or already-released id as a no-op. This is why every message carries the ATM's transaction id: it is the idempotency key.
- Dispense fault after the hold. Some notes may have been picked before the jam. They go to the reject bin, not to the customer, so the customer is not charged and the cassette counts drop. Reconciliation at the next cash replenishment counts the reject bin to prove it.
- Customer does not take the cash. Many ATMs retract presented notes after a timeout and reverse the withdrawal: one more state between dispense and commit.
- Power cut mid-transaction. On restart, read the journal. Any transaction with a hold but no final outcome gets a release (or a commit if the dispenser's sensors confirm notes were presented). The journal must be written before the hold is sent, not after.
- Daily limit at midnight. Compute the day from the injected clock, in the bank's time zone. A limit tracked in the ATM is only a first line of defence; the bank enforces the real one across all machines.
- Wrong PIN count. The count here is per session; the bank also keeps a per-card count across machines.
Down the stack
Layer 1The contract
withdraw(amount) either returns a note plan whose total equals amount, with the customer debited exactly amount once, or fails with a reason and leaves the customer's balance and the daily total unchanged. Cassette counts drop only when notes physically leave a cassette (dispensed or rejected). For every transaction id the bank receives at most one final message, commit or release, and repeated releases are harmless. Card retention happens on the third wrong PIN in a session and ends the session.
Layer 2The mechanism
On the network, a withdrawal is an ISO 8583 financial request (message type 0200) carrying the card number, amount, a terminal id, a system trace audit number and an encrypted PIN block; the host answers with 0210 and a response code, where 00 means approved and 51 means insufficient funds. If the ATM cannot complete, it sends a reversal (0420 reversal advice) with the original trace number, and keeps resending it from a store-and-forward queue until acknowledged. The PIN never leaves the encrypting PIN pad in clear: the pad builds an ISO 9564 PIN block, encrypts it with a key loaded under dual control, and the bank's hardware security module translates and verifies it. In the cabinet, the dispenser picks notes one at a time from each cassette, measures thickness to detect two notes stuck together, and diverts anything suspicious to the reject bin before stacking the rest for presentation.
Layer 3The machine
The bank round trip is the slow part: the ATM's network link, the acquirer switch, the card scheme and the issuer host add up to roughly 200 ms to 2 s, and hosts commonly time out a request after around 30 seconds. The dispenser picks several notes per second, so a 10-note withdrawal takes a few seconds of mechanical time. The fewest-notes search over three or four denominations takes microseconds. Durability matters more than speed: the electronic journal is written to local flash or disk and flushed before the hold is sent, because after a power cut it is the only record of what was in flight. An fsync on flash costs on the order of milliseconds, which disappears against a network round trip.
Why it's built this way
- Cash is physical and cannot be recalled, so the bank must not finalise a debit until the machine knows the notes left.
- The network can fail between any two steps, so the protocol is hold, act, then commit or release, with the transaction id as an idempotency key.
- Reversals can also be lost, so they are queued durably and resent until acknowledged, and the bank must accept duplicates.
- A stolen card is worth nothing without the PIN, so the PIN is encrypted in tamper-resistant hardware and guessing is limited by retaining the card.
- Hardware fails in ways software cannot see directly, so the reject bin and journal give a physical audit trail that reconciliation can check.
Extensibility
- Deposits. A new command: accept notes into escrow, validate them, ask the bank to credit, then move escrow into a deposit cassette or return it. The same template method applies, with the physical step first and the bank step second.
- Card-less withdrawal. A new authentication path: the customer scans a one-time code from a phone app, and the session becomes authenticated without PIN entry. The State machine gains one transition, not a new class hierarchy.
- Fees and foreign cards. A fee policy strategy that takes the card's issuer and returns a surcharge, shown on screen for consent before the hold. The hold amount becomes amount plus fee.
Leadership lens
- Build first: the transaction protocol with a fake bank and a fake dispenser that can jam, time out and reply twice. Screens and receipts are easy; the support cost of "I was charged but got no cash" is not.
- Ask in review: when exactly is the journal written relative to the hold; what happens if the release is lost; how is the reject bin reconciled; who resets an out-of-service machine and how is that audited.
- ATM software sits in a regulated, certified environment. Changes to PIN handling or the bank interface need scheme and security certification, so keep those parts small, stable and separate from the screens marketing wants to change.
- The cost drivers are cash logistics and downtime, not compute. A planner that keeps a usable mix of notes (do not drain the 20s first) can delay a replenishment visit, which is real money.
Check yourself
Why does the ATM plan the notes before it asks the bank for a hold?
Show answer
Whether the cassettes can make the amount is a free local check. Asking the bank first creates a hold that must then be released, adding traffic and a window where the customer's funds are tied up for nothing.
The hold request times out with no reply. What should the ATM do?
Show answer
Treat the outcome as unknown, refuse the withdrawal, and send a release for that transaction id. The bank makes release idempotent, so it is safe whether the hold existed or not.
Why does greedy dispensing fail with 50s and 20s, and what replaces it?
Show answer
Taking the largest notes first can leave a remainder no smaller note can pay (110 becomes 100 plus an unpayable 10). A bounded search or dynamic program over note counts finds 50 + 20 + 20 + 20.
After a jam, why do the cassette counts go down even though the customer received nothing?
Show answer
The notes physically left the cassettes and went to the reject bin. Counting them as still in the cassette would make the next reconciliation show a shortage that looks like theft.
Watch
- State Pattern, Design Patterns ep 17 (Christopher Okhravi): the pattern behind the session states, with a careful explanation of when it pays off.
- Command Pattern (Christopher Okhravi): why transactions as objects make journaling and undo natural.
Free resources
- ISO 8583 (Wikipedia): message types, bitmaps and response codes used between ATMs and banks.
- Automated teller machine (Wikipedia): hardware components, security and history.
- CEN/XFS (Wikipedia): the device-control standard that lets ATM software drive many vendors' hardware.
- Making retries safe with idempotent APIs (AWS Builders' Library): the reasoning behind idempotent release and reversal messages.
- State pattern (Refactoring.Guru): when to move from an enum to state objects.