LLD CASE STUDIES / OBJECT DESIGN BRIEF

Design an ATM

Design an ATM.

IntermediatePhase 09 / Topic 10 of 10ResponsibilitiesCollaborationsExtensibility
01

Overview

Design an ATM. Requirements: a customer inserts a card, enters a PIN (with limited attempts), and then checks balance, withdraws cash, or deposits; the ATM talks to the bank to authenticate and authorize transactions; it dispenses cash in available denominations; it prints receipts; and it handles failures such as insufficient funds, daily limits, cash shortages, and dispense errors without charging the customer.

The design combines several patterns: State for the session (Idle, CardInserted, Authenticated, Transaction), Chain of Responsibility for dispensing denominations, Facade or adapter for the bank network, Command/Strategy for transaction types, and careful two-step money handling (hold, dispense, then commit, or release on failure).

A bank teller in a box

The machine verifies who you are, asks the bank if you can have the money, counts notes from its drawers, and only finalizes the withdrawal once the cash is out. If the drawer jams, it tells the bank to cancel.

02

When to use it

  • Frequent LLD interview question.
  • Practicing State and Chain of Responsibility together.
  • Modeling external systems and failure recovery.
03

Where it shows up in interviews

Session state machine

Recognize it when: flow of steps with security rules.

  • Design an ATM
  • Design a kiosk
  • Design a vending machine
Denomination breakdown

Recognize it when: split an amount into available units.

  • Design an ATM cash dispenser
  • Design ATM (LeetCode 2241)
04

Where it is used in real software

ISO 8583 bank messaging

ATMs send authorization and reversal messages to the bank network.

Reversal on dispense failure

If cash is not dispensed, the ATM sends a reversal so the account is not debited.

XFS middleware

Standard APIs abstract card readers, dispensers, and printers from vendors.

05

Key terms

Session
Card inserted to card ejected.
Authorization hold
Bank reserves funds before dispensing.
Reversal
Cancel a hold or debit after failure.
Cash cassette
Drawer of one denomination with a count.
Daily limit
Maximum withdrawal per card per day.
06

How it works, step by step

  1. 1
    Clarify

    Transactions, denominations, limits, PIN attempts, deposits, receipts.

  2. 2
    Entities

    ATM, Session/State, Card, BankService, CashDispenser (chain), Transaction types, Receipt.

  3. 3
    Authenticate

    Bank validates PIN; 3 failures retain the card.

  4. 4
    Withdraw flow

    Check dispensable, bank hold, dispense, commit; release on failure.

  5. 5
    End session

    Print receipt, eject card, return to Idle.

07

Withdraw 280 with cassettes {100: 2, 50: 1, 20: 5}

Chain: 100 -> 50 -> 20

Step 1 / 5
HandlerNotes usedRemaining
$100280
$50130
$20110
End of chain-10 left: cannot dispense
Retry plan: 100x2, 20x4Valid0

NOWHandler: $100 | Notes used: 2 | Remaining: 80

Pure greedy can fail with limited notes; plan the combination (with backtracking or DP over counts) before touching the bank or cassettes.

08

Implementation

import java.util.*; interface BankService {    boolean verifyPin(String card, String pin);    long balance(String card);    Optional<String> hold(String card, long amount);   // returns hold id if authorized    void commit(String holdId);    void release(String holdId);} final class CashDispenser {    private final TreeMap<Integer, Integer> cassettes = new TreeMap<>(Comparator.reverseOrder()); // denomination -> count     void load(int denomination, int count) { cassettes.merge(denomination, count, Integer::sum); }     /** Plan notes with backtracking over denominations so limited counts do not break greedy. */    Optional<Map<Integer, Integer>> plan(int amount) {        List<Integer> denoms = new ArrayList<>(cassettes.keySet());        Map<Integer, Integer> chosen = new LinkedHashMap<>();        return search(amount, 0, denoms, chosen) ? Optional.of(chosen) : Optional.empty();    }     private boolean search(int remaining, int i, List<Integer> denoms, Map<Integer, Integer> chosen) {        if (remaining == 0) return true;        if (i == denoms.size()) return false;        int d = denoms.get(i);        for (int n = Math.min(remaining / d, cassettes.get(d)); n >= 0; n--) {            if (n > 0) chosen.put(d, n); else chosen.remove(d);            if (search(remaining - n * d, i + 1, denoms, chosen)) return true;        }        chosen.remove(d);        return false;    }     void dispense(Map<Integer, Integer> notes) {        notes.forEach((d, n) -> cassettes.merge(d, -n, Integer::sum));    }} final class Atm {    enum State { IDLE, CARD_INSERTED, AUTHENTICATED }    private static final int MAX_PIN_ATTEMPTS = 3;    private static final long DAILY_LIMIT = 1_000;     private final BankService bank;    private final CashDispenser dispenser;    private State state = State.IDLE;    private String card;    private int pinAttempts;    private final Map<String, Long> withdrawnToday = new HashMap<>();     Atm(BankService bank, CashDispenser dispenser) { this.bank = bank; this.dispenser = dispenser; }     void insertCard(String card) {        require(State.IDLE);        this.card = card; pinAttempts = 0; state = State.CARD_INSERTED;    }     boolean enterPin(String pin) {        require(State.CARD_INSERTED);        if (bank.verifyPin(card, pin)) { state = State.AUTHENTICATED; return true; }        if (++pinAttempts >= MAX_PIN_ATTEMPTS) { retainCard(); }        return false;    }     long balance() { require(State.AUTHENTICATED); return bank.balance(card); }     String withdraw(int amount) {        require(State.AUTHENTICATED);        if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");        if (withdrawnToday.getOrDefault(card, 0L) + amount > DAILY_LIMIT) return "Daily limit exceeded";         Optional<Map<Integer, Integer>> notes = dispenser.plan(amount);        if (notes.isEmpty()) return "Cannot dispense this amount; try another";         Optional<String> hold = bank.hold(card, amount);        if (hold.isEmpty()) return "Insufficient funds";        try {            dispenser.dispense(notes.get());            bank.commit(hold.get());            withdrawnToday.merge(card, (long) amount, Long::sum);            return "Please take your cash: " + notes.get();        } catch (RuntimeException jam) {            bank.release(hold.get());                               // never charge for undispensed cash            return "Dispense failed; you were not charged";        }    }     void eject() { card = null; state = State.IDLE; }    private void retainCard() { card = null; state = State.IDLE; }    private void require(State expected) { if (state != expected) throw new IllegalStateException("Invalid in state " + state); }}
09

Complexity and performance

Dispense planningSmall backtracking

Few denominations; bounded counts.

Session operationsO(1) + bank latency

Network-bound.

10

Trade-offs

Greedy vs planned dispensing

Greedy is simple but can fail with limited notes; planning first avoids partial dispenses.

Hold-then-commit vs direct debit

Holds add a round trip but make failures recoverable without customer loss.

11

Variants and related techniques

Deposits

Envelope or cash recycler flows with pending credit until verified.

State pattern classes

Replace the enum with IdleState, CardState, AuthenticatedState classes.

12

Common mistakes

  • Debiting before dispensing with no reversal.

    Fix: Hold, dispense, then commit; release on failure.

  • Allowing operations in the wrong state.

    Fix: Guard every operation by session state.

  • Unlimited PIN attempts.

    Fix: Retain the card after a fixed number of failures.

13

Interview questions

How do you ensure a customer is not charged when cash fails to dispense?

Use a two-step flow: ask the bank for an authorization hold, dispense cash, and only then commit. If dispensing fails, release the hold (reversal). Check dispensability before contacting the bank.

Which design patterns fit an ATM?

State for the session lifecycle, Chain of Responsibility or a planner for denominations, Facade/Adapter for the bank network, Command or Strategy for transaction types, and Observer for alerts like low cash.

14

Practice problems

ProblemDifficultyWhat it trains
Design an ATM Machine (LeetCode 2241)MediumDenomination planning.
Full ATM with states, limits, and reversalsHardFailure handling.