Engineering Atlas › Low-Level Design

Code a cash machine (ATM)

12 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

Requirements

Functional:

Non-functional:

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

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 --> Clock

Key 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
    end

Design patterns used and why

Concurrency and edge cases

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

  1. Cash is physical and cannot be recalled, so the bank must not finalise a debit until the machine knows the notes left.
  2. 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.
  3. Reversals can also be lost, so they are queued durably and resent until acknowledged, and the bank must accept duplicates.
  4. 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.
  5. Hardware fails in ways software cannot see directly, so the reject bin and journal give a physical audit trail that reconciliation can check.

Extensibility

Leadership lens

Check yourself

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

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

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

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

Free resources

Open in the interactive app: practice, quiz, mock interview →