Design an ATM Machine, Low Level Design (LLD) Interview
An ATM that moves through clear states, keeps a card after 3 wrong PINs, plans which notes to pay out before it takes any money, debits each withdrawal only once, and gives the money back if the cash never leaves the machine.
Where it shows up
A classic object-oriented design question. It shows up in LLD and machine-coding rounds next to the parking lot and the vending machine.
Two courses by the author of this page:
770 lessons · 18 free to read
₹499 in India · $49 elsewhere, once
Get System DesignYou own this course
204 lessons · 10 free to read
₹999 in India · $49 elsewhere, once
Get AI EngineeringYou own this course
Spec sheetATM
- 01Core classes
- Atm, AtmState, BankService, Account, CashDispenser, NoteHandler, Cassette
- 02Design patterns
- State, Chain of Responsibility, Strategy, behind an interface, Idempotent request with a reversal
- 03Enums
- PinResult, TxnStatus
- 04Key methods
- 4 signatures, with the code skeleton
worked through below, with the maths
Why this is asked
Everyone has used an ATM, so no time is lost on the problem. The design still has real traps. One button does different things at different moments, which is a test of the State pattern. It must pay an amount out of a few kinds of notes, which hides a small algorithm. And it moves money over a network that can fail at the worst time. A strong answer shows what happens when the bank's reply is lost, or when the cash jams after the account was already charged.
Requirements
Functional
- A person inserts a card and enters a PIN. A PIN is the short secret number for the card.
- After 3 wrong PINs in a row, the machine keeps the card and the bank blocks it.
- A person with a verified PIN can withdraw cash and check the balance.
- The machine pays an amount using the notes it holds, for example $100, $50, $20 and $10 notes.
- If the machine cannot make the amount from its notes, it says so before any money is taken.
- A person can cancel at any point before the cash is counted out, and gets the card back.
Constraints & non-functional
- The account is charged at most once for one withdrawal, even if the request is sent twice.
- If the account was charged but no cash came out, the charge is reversed.
- Two machines using one shared account at once can never take the balance below zero.
- The ATM talks to the bank only through an interface, so a test bank and a real bank can be swapped.
- Each button press does only what the current state allows. A withdraw before a PIN is ignored.
Core classes & entities
Atm
The machine. It holds the current state, the card reader slot, the dispenser and a link to the bank. Every button press is handed to the current state.
attrs: state: AtmState, dispenser: CashDispenser, bank: BankService, card
methods: insertCard(card), enterPin(pin), withdraw(amount), cancel()
AtmState
One screen of the machine. Each state decides what a button does in that moment and which state comes next. The states are idle, card inserted, PIN verified, transaction and dispensing.
methods: insertCard(atm, card), enterPin(atm, pin), withdraw(atm, amount), cancel(atm)
BankService
The interface to the bank. It checks a PIN, debits an account once per transaction id, and reverses a debit. A debit means taking money out of an account.
methods: verifyPin(card, pin): PinResult, withdraw(txnId, card, amount): TxnStatus, reverse(txnId): TxnStatus, balance(card)
Account
One balance, guarded by a lock. A thread is one line of work running inside a program. A lock lets only one thread change the balance at a time, so two withdrawals cannot both spend one dollar.
attrs: balance: long
methods: debit(amount): boolean, credit(amount), balance()
CashDispenser
Holds the note cassettes, plans which notes make an amount, and pays them out. A cassette is the box inside the machine that holds one kind of note.
attrs: cassettes: List<Cassette>, head: NoteHandler
methods: greedy(amount), exact(amount), planFor(amount), dispense(plan): boolean
NoteHandler
One link in the dispenser chain. It takes as many of its own notes as fit and passes the rest of the amount to the next, smaller note.
attrs: next: NoteHandler
methods: plan(amount, out): boolean, linkTo(next)
Cassette
A NoteHandler for one note value, with a count of how many notes are left.
attrs: note, count
methods: note(), available()
Relationships
- Atm → association → AtmState. The ATM points at exactly one state at a time and swaps it as the session moves on.
- Atm → composition → CashDispenser. The dispenser is a part of the machine.
- Atm → association → BankService. The bank lives elsewhere. The ATM only holds a reference to it.
- CashDispenser → composition → Cassette. The cassettes sit inside the dispenser.
- NoteHandler → inheritance → Cassette. A cassette is one link of the chain.
- BankService → association → Account. The bank finds the account for a card and debits it.
Design patterns used
State in IdleState, CardInsertedState, PinVerifiedState, TransactionState, DispensingState
One button means different things at different moments. Each state is its own class, so there is no big switch statement, and a new screen is a new class.
Chain of Responsibility in NoteHandler and the cassettes, biggest note first
Each note box handles what it can and passes the rest on. A new note value is one more link in the chain.
Strategy, behind an interface in BankService with InMemoryBank
The machine logic is tested against a bank in memory. A real bank client can be plugged in with no change to the states.
Idempotent request with a reversal in BankService.withdraw(txnId, ...) and reverse(txnId)
Idempotent means doing it twice has an effect equal to doing it once. A retry that reuses the id cannot charge twice, and a failed payout is undone.
Enums
Key API / methods
void Atm.withdraw(long amount)Passed to the current state. In the PIN verified state it starts a transaction. In any earlier state it is ignored.
Map<Integer,Integer> CashDispenser.planFor(long amount)Returns which notes to pay, for example 3 notes of $20. It tries the fast chain first. If the chain gets stuck, or uses more notes than needed, it uses an exact search. It returns null if the amount cannot be made.
TxnStatus BankService.withdraw(String txnId, String card, long amount)Debits the account once for this transaction id. A second call with that id returns the first answer and moves no money.
TxnStatus BankService.reverse(String txnId)Gives a debited amount back once. Calling it again does nothing.
Code skeleton
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
// ---------- Enums ----------
enum PinResult { OK, WRONG, BLOCKED }
enum TxnStatus { DEBITED, REVERSED, DECLINED }
// ---------- Bank side: the ATM only talks to this interface ----------
interface BankService {
PinResult verifyPin(String card, String pin);
/** Debit once per txnId. Calling again with the same txnId returns the first answer and moves no money. */
TxnStatus withdraw(String txnId, String card, long amount);
/** Put the money back once. A second call does nothing. */
TxnStatus reverse(String txnId);
long balance(String card);
}
final class Account {
private long balance; // whole dollars; real systems use cents in a long
Account(long b) { balance = b; }
synchronized boolean debit(long amt) { if (amt <= 0 || amt > balance) return false; balance -= amt; return true; }
synchronized void credit(long amt) { balance += amt; }
synchronized long balance() { return balance; }
}
final class InMemoryBank implements BankService {
static final int MAX_PIN_TRIES = 3;
private final Map<String, String> pins = new ConcurrentHashMap<>();
private final Map<String, Account> accounts = new ConcurrentHashMap<>();
private final Map<String, Integer> wrongTries = new ConcurrentHashMap<>();
private final Map<String, TxnStatus> txns = new ConcurrentHashMap<>();
private final Map<String, long[]> txnAmount = new ConcurrentHashMap<>();
private final Map<String, String> txnCard = new ConcurrentHashMap<>();
final AtomicInteger debitsApplied = new AtomicInteger();
void open(String card, String pin, long balance) { pins.put(card, pin); accounts.put(card, new Account(balance)); }
public PinResult verifyPin(String card, String pin) {
if (wrongTries.getOrDefault(card, 0) >= MAX_PIN_TRIES) return PinResult.BLOCKED;
if (pins.get(card).equals(pin)) { wrongTries.put(card, 0); return PinResult.OK; }
int n = wrongTries.merge(card, 1, Integer::sum);
return n >= MAX_PIN_TRIES ? PinResult.BLOCKED : PinResult.WRONG;
}
public TxnStatus withdraw(String txnId, String card, long amount) {
// computeIfAbsent runs the debit at most once per txnId, even if two retries race
return txns.computeIfAbsent(txnId, id -> {
if (!accounts.get(card).debit(amount)) return TxnStatus.DECLINED;
debitsApplied.incrementAndGet();
txnAmount.put(id, new long[]{amount}); txnCard.put(id, card);
return TxnStatus.DEBITED;
});
}
public TxnStatus reverse(String txnId) {
return txns.computeIfPresent(txnId, (id, st) -> {
if (st != TxnStatus.DEBITED) return st; // already reversed, or never debited
accounts.get(txnCard.get(id)).credit(txnAmount.get(id)[0]);
return TxnStatus.REVERSED;
});
}
public long balance(String card) { return accounts.get(card).balance(); }
}
// ---------- Cash dispenser: a chain of note handlers, biggest note first ----------
abstract class NoteHandler {
private NoteHandler next;
NoteHandler linkTo(NoteHandler n) { next = n; return n; }
/** Take as many of my notes as fit, pass the rest down the chain. Returns false if the chain cannot finish. */
boolean plan(long amount, Map<Integer, Integer> out) {
int take = (int) Math.min(amount / note(), available());
if (take > 0) out.put(note(), take);
long rest = amount - (long) take * note();
if (rest == 0) return true;
return next != null && next.plan(rest, out);
}
abstract int note(); abstract int available();
}
final class Cassette extends NoteHandler {
final int note; int count;
Cassette(int note, int count) { this.note = note; this.count = count; }
int note() { return note; } int available() { return count; }
}
final class CashDispenser {
private final List<Cassette> cassettes; // sorted, biggest note first
private final NoteHandler head;
boolean jamNextDispense; // test hook: the hardware fails once
CashDispenser(List<Cassette> cs) {
cassettes = new ArrayList<>(cs); cassettes.sort((a, b) -> b.note - a.note);
for (int i = 0; i + 1 < cassettes.size(); i++) cassettes.get(i).linkTo(cassettes.get(i + 1));
head = cassettes.get(0);
}
/** Greedy walk down the chain. Fast, and best for canonical note sets with enough stock. */
Map<Integer, Integer> greedy(long amount) { Map<Integer, Integer> m = new TreeMap<>(Comparator.reverseOrder()); return head.plan(amount, m) ? m : null; }
/** Exact plan with fewest notes that respects the stock in each cassette. A small dynamic program over the amount. */
Map<Integer, Integer> exact(long amount) {
if (amount > 100_000) return null;
int a = (int) amount; int[] best = new int[a + 1]; int[][] used = new int[a + 1][];
Arrays.fill(best, Integer.MAX_VALUE); best[0] = 0; used[0] = new int[cassettes.size()];
for (int x = 1; x <= a; x++)
for (int i = 0; i < cassettes.size(); i++) {
int n = cassettes.get(i).note; if (n > x || best[x - n] == Integer.MAX_VALUE) continue;
if (used[x - n][i] >= cassettes.get(i).count || best[x - n] + 1 >= best[x]) continue;
best[x] = best[x - n] + 1; used[x] = used[x - n].clone(); used[x][i]++;
}
if (best[a] == Integer.MAX_VALUE) return null;
Map<Integer, Integer> m = new TreeMap<>(Comparator.reverseOrder());
for (int i = 0; i < cassettes.size(); i++) if (used[a][i] > 0) m.put(cassettes.get(i).note, used[a][i]);
return m;
}
/** Try greedy first. If it fails or uses more notes than needed, use the exact plan. */
Map<Integer, Integer> planFor(long amount) {
Map<Integer, Integer> g = greedy(amount), e = exact(amount);
if (g == null) return e;
return (e != null && notes(e) < notes(g)) ? e : g;
}
static int notes(Map<Integer, Integer> m) { return m.values().stream().mapToInt(Integer::intValue).sum(); }
boolean dispense(Map<Integer, Integer> plan) {
if (jamNextDispense) { jamNextDispense = false; return false; } // nothing left the machine
for (Cassette c : cassettes) c.count -= plan.getOrDefault(c.note, 0);
return true;
}
long cashInside() { return cassettes.stream().mapToLong(c -> (long) c.note * c.count).sum(); }
}
// ---------- The ATM and its states (State pattern) ----------
interface AtmState {
default void insertCard(Atm atm, String card) { atm.say("ignored: insert card in " + name()); }
default void enterPin(Atm atm, String pin) { atm.say("ignored: PIN in " + name()); }
default void withdraw(Atm atm, long amount) { atm.say("ignored: withdraw in " + name()); }
default void cancel(Atm atm) { atm.ejectCard(); atm.setState(new IdleState()); }
String name();
}
final class IdleState implements AtmState {
public String name() { return "IDLE"; }
public void insertCard(Atm atm, String card) { atm.card = card; atm.setState(new CardInsertedState()); }
public void cancel(Atm atm) { }
}
final class CardInsertedState implements AtmState {
public String name() { return "CARD_INSERTED"; }
public void enterPin(Atm atm, String pin) {
switch (atm.bank.verifyPin(atm.card, pin)) {
case OK -> atm.setState(new PinVerifiedState());
case WRONG -> atm.say("wrong PIN, try again");
case BLOCKED -> { atm.say("too many wrong PINs, card kept"); atm.retainCard(); atm.setState(new IdleState()); }
}
}
}
final class PinVerifiedState implements AtmState {
public String name() { return "PIN_VERIFIED"; }
public void withdraw(Atm atm, long amount) { atm.setState(new TransactionState()); atm.state.withdraw(atm, amount); }
}
final class TransactionState implements AtmState {
public String name() { return "TRANSACTION"; }
public void withdraw(Atm atm, long amount) {
Map<Integer, Integer> plan = atm.dispenser.planFor(amount); // 1. can this machine pay it out at all?
if (plan == null) { atm.say("cannot make " + amount + " from the notes inside"); atm.setState(new PinVerifiedState()); return; }
String txnId = atm.nextTxnId();
TxnStatus st = atm.bank.withdraw(txnId, atm.card, amount); // 2. debit, once per txnId
if (atm.simulateLostReply) { // the reply was lost: retry with the SAME id
atm.simulateLostReply = false; st = atm.bank.withdraw(txnId, atm.card, amount);
}
if (st != TxnStatus.DEBITED) { atm.say("declined"); atm.setState(new PinVerifiedState()); return; }
atm.setState(new DispensingState());
((DispensingState) atm.state).dispense(atm, txnId, plan);
}
}
final class DispensingState implements AtmState {
public String name() { return "DISPENSING"; }
public void cancel(Atm atm) { atm.say("ignored: cannot cancel while dispensing"); }
void dispense(Atm atm, String txnId, Map<Integer, Integer> plan) {
if (atm.dispenser.dispense(plan)) { atm.lastNotes = plan; atm.say("dispensed " + plan); }
else { atm.bank.reverse(txnId); atm.say("dispense failed, debit reversed"); } // 3. no cash out, money goes back
atm.ejectCard(); atm.setState(new IdleState());
}
}
final class Atm {
final BankService bank; final CashDispenser dispenser; AtmState state = new IdleState();
String card; final List<String> log = new ArrayList<>(); final List<String> retained = new ArrayList<>();
Map<Integer, Integer> lastNotes; boolean simulateLostReply; private int seq; private final String id;
Atm(String id, BankService b, CashDispenser d) { this.id = id; bank = b; dispenser = d; }
void setState(AtmState s) { state = s; }
void say(String s) { log.add(s); }
String nextTxnId() { return id + "-" + (++seq); }
void ejectCard() { card = null; }
void retainCard() { retained.add(card); card = null; }
// the buttons a person presses, each forwarded to the current state
void insertCard(String c) { state.insertCard(this, c); }
void enterPin(String p) { state.enterPin(this, p); }
void withdraw(long a) { state.withdraw(this, a); }
void cancel() { state.cancel(this); }
}
// ---------- Demo with checks ----------
public class AtmDemo {
public static void main(String[] args) throws Exception {
System.out.println("Session flow and the PIN limit");
InMemoryBank bank = new InMemoryBank();
bank.open("card-A", "1234", 1_000); bank.open("card-B", "9999", 500);
Atm atm = new Atm("atm1", bank, new CashDispenser(List.of(new Cassette(100, 10), new Cassette(50, 10), new Cassette(20, 20))));
atm.withdraw(100);
check(atm.state instanceof IdleState && bank.balance("card-A") == 1_000, "withdraw before a card is inserted is ignored");
atm.insertCard("card-A"); atm.enterPin("1234"); atm.withdraw(260);
check(bank.balance("card-A") == 740 && atm.lastNotes.equals(Map.of(100, 2, 20, 3)) && atm.state instanceof IdleState,
"withdraw 260 from 100/50/20 notes: greedy (100,100,50) is stuck at 10, the exact plan pays " + atm.lastNotes + ", balance 1000 -> 740");
atm.insertCard("card-B");
atm.enterPin("0000"); atm.enterPin("1111");
check(atm.state instanceof CardInsertedState, "2 wrong PINs: still waiting for the PIN");
atm.enterPin("2222");
check(atm.retained.equals(List.of("card-B")) && atm.state instanceof IdleState, "3rd wrong PIN: card kept by the machine, back to IDLE");
atm.insertCard("card-B"); atm.enterPin("9999");
check(atm.state instanceof IdleState, "the bank keeps the block: even the right PIN is refused now");
System.out.println("Cash dispenser (Chain of Responsibility)");
CashDispenser us = new CashDispenser(List.of(new Cassette(100, 50), new Cassette(50, 50), new Cassette(20, 50), new Cassette(10, 50)));
check(us.greedy(380).equals(Map.of(100, 3, 50, 1, 20, 1, 10, 1)), "380 with 100/50/20/10 notes, greedy: " + us.greedy(380));
int worse = 0;
for (int a = 10; a <= 2_000; a += 10) if (CashDispenser.notes(us.greedy(a)) != CashDispenser.notes(us.exact(a))) worse++;
check(worse == 0, "100/50/20/10 is canonical: greedy uses the fewest notes for all 200 amounts from 10 to 2,000");
CashDispenser odd = new CashDispenser(List.of(new Cassette(25, 50), new Cassette(20, 50), new Cassette(5, 50), new Cassette(1, 50)));
check(CashDispenser.notes(odd.greedy(40)) == 4 && CashDispenser.notes(odd.exact(40)) == 2,
"25/20/5/1 is not canonical: for 40 greedy gives " + odd.greedy(40) + " (4 notes), best is " + odd.exact(40) + " (2 notes)");
check(odd.planFor(40).equals(Map.of(20, 2)), "planFor(40) picks the 2-note plan");
CashDispenser low = new CashDispenser(List.of(new Cassette(50, 1), new Cassette(20, 3)));
check(low.greedy(60) == null && low.planFor(60).equals(Map.of(20, 3)), "stock 1 x 50 and 3 x 20: greedy fails on 60 (50 + ?), exact finds 3 x 20");
check(low.planFor(30) == null, "30 cannot be made from 50s and 20s at all: refused before any debit");
System.out.println("Money safety: refuse early, debit once, reverse on a failed dispense");
InMemoryBank b2 = new InMemoryBank(); b2.open("card-C", "4321", 500);
Atm atm2 = new Atm("atm2", b2, low);
atm2.insertCard("card-C"); atm2.enterPin("4321"); atm2.withdraw(30);
check(b2.balance("card-C") == 500 && atm2.state instanceof PinVerifiedState, "amount the machine cannot make: no debit, choose again");
atm2.simulateLostReply = true; atm2.withdraw(60);
check(b2.balance("card-C") == 440 && b2.debitsApplied.get() == 1, "bank reply lost, ATM retries with the same txn id: debited once, balance 440");
atm2.insertCard("card-C"); atm2.enterPin("4321");
low.jamNextDispense = true; long cashBefore = low.cashInside();
atm2.withdraw(50);
check(b2.balance("card-C") == 440 && low.cashInside() == cashBefore, "jam during dispense: debit of 50 reversed, balance back to 440, cash inside unchanged");
check(b2.reverse("atm2-2") == TxnStatus.REVERSED && b2.balance("card-C") == 440, "reversing the same txn again moves no money");
System.out.println("Many ATMs, one account, at the same time");
InMemoryBank b3 = new InMemoryBank(); b3.open("joint", "1", 50_000);
ExecutorService pool = Executors.newFixedThreadPool(8);
AtomicInteger ok = new AtomicInteger(); List<Future<?>> fs = new ArrayList<>();
for (int t = 0; t < 8; t++) { final int tt = t; fs.add(pool.submit(() -> {
for (int i = 0; i < 250; i++) if (b3.withdraw("t" + tt + "-" + i, "joint", 100) == TxnStatus.DEBITED) ok.incrementAndGet();
})); }
for (Future<?> f : fs) f.get(); pool.shutdown();
check(ok.get() == 500 && b3.balance("joint") == 0, "8 threads try 2,000 withdrawals of 100 from 50,000: exactly 500 succeed, balance 0, never below");
}
static void check(boolean ok, String what) { System.out.println((ok ? " ok " : " FAIL ") + what); if (!ok) System.exit(1); }
}
/* Output of this exact program (javac + java 21, 2026-10-07):
* Session flow and the PIN limit
* ok withdraw before a card is inserted is ignored
* ok withdraw 260 from 100/50/20 notes: greedy (100,100,50) is stuck at 10, the exact plan pays {100=2, 20=3}, balance 1000 -> 740
* ok 2 wrong PINs: still waiting for the PIN
* ok 3rd wrong PIN: card kept by the machine, back to IDLE
* ok the bank keeps the block: even the right PIN is refused now
* Cash dispenser (Chain of Responsibility)
* ok 380 with 100/50/20/10 notes, greedy: {100=3, 50=1, 20=1, 10=1}
* ok 100/50/20/10 is canonical: greedy uses the fewest notes for all 200 amounts from 10 to 2,000
* ok 25/20/5/1 is not canonical: for 40 greedy gives {25=1, 5=3} (4 notes), best is {20=2} (2 notes)
* ok planFor(40) picks the 2-note plan
* ok stock 1 x 50 and 3 x 20: greedy fails on 60 (50 + ?), exact finds 3 x 20
* ok 30 cannot be made from 50s and 20s at all: refused before any debit
* Money safety: refuse early, debit once, reverse on a failed dispense
* ok amount the machine cannot make: no debit, choose again
* ok bank reply lost, ATM retries with the same txn id: debited once, balance 440
* ok jam during dispense: debit of 50 reversed, balance back to 440, cash inside unchanged
* ok reversing the same txn again moves no money
* Many ATMs, one account, at the same time
* ok 8 threads try 2,000 withdrawals of 100 from 50,000: exactly 500 succeed, balance 0, never below
*/How it works

Start with the states, because the rest hangs off them. A state is one moment of the session: idle, card inserted, PIN verified, transaction, and dispensing. The Atm class holds the current state and hands every button press to it. In the idle state, only inserting a card does anything. In the card inserted state, only a PIN does anything. This is the State pattern. It stops bugs like taking money before the PIN was checked, because that button simply does nothing in the wrong state.
The PIN limit belongs to the bank, not to the machine. The program counts wrong tries per card in the bank. After 3, the bank answers BLOCKED and the machine keeps the card. If the count lived only in the machine, a person could walk to the next ATM and get 3 more tries.
Next comes the cash. The dispenser is a chain of note handlers, biggest note first. Each takes as many of its notes as fit and passes the rest down. This is called the greedy method. For a note set like $100, $50, $20 and $10, greedy always uses the fewest notes. The program checks all 200 amounts from $10 to $2,000 and finds no case where it does worse. A note set where greedy is always best is called canonical. Some sets are not. With notes of 25, 20, 5 and 1, greedy pays 40 as 25 plus three 5s, which is 4 notes. Two 20s is better. A bigger problem shows up with real stock. A machine with one $50 note and three $20 notes gets stuck on $60, because greedy takes the $50 first. So the dispenser always runs a small exact search as a backup, and keeps whichever plan uses fewer notes. If neither works, the amount is refused.
The order of steps is what keeps money safe. First, plan the notes, so the machine never charges for an amount it cannot pay. Second, ask the bank to debit, with a transaction id made by the machine. Third, count out the notes. If the bank's reply is lost, the machine sends the request again with that id. The bank sees an id it already handled and returns the first answer, so the account is charged once. If the notes jam, the machine calls reverse with that id and the money goes back. Real card networks have a message for this. ISO 8583, the standard for card payment messages, has a group of reversal messages, such as 0420, the acquirer reversal advice.
Last, two machines can use one shared account at once. The Account class locks its balance while it checks and subtracts. The program runs 8 threads that try 2,000 withdrawals of $100 from $50,000. Exactly 500 succeed and the balance ends at 0. It never goes below.
Good follow-ups to mention: a deposit flow, a daily withdrawal limit, a printed receipt, and what happens if power fails in the middle of a payout. For the last one, the machine writes each step to its own log before doing it. When it starts again, it reads the log and sends any reversal that is still owed.
Edge cases & gotchas
- The machine cannot make the amount, for example $30 from $50 and $20 notes. It says so before the account is touched.
- The fast chain gets stuck even though an answer exists. With one $50 note and three $20 notes, the chain takes the $50 for a $60 request and cannot finish. The exact search pays three $20 notes.
- The bank charged the account but its reply was lost on the network. The ATM retries with that transaction id, so the account is charged once.
- The cash jams after the debit. The ATM reverses the debit, and the note counts inside do not change.
- Two machines withdraw from one shared account at once. The lock on the account means the balance never goes below zero.
- A third wrong PIN. The card is kept, and the bank refuses the card even with the right PIN after that.
- Cancel while notes are being counted out is refused. Before that point, cancel returns the card.
FAQ
Master LLD and system design interviews
770 interactive lessons and 90 real systems taken apart. One payment, lifetime access, no subscription.
course 1
System Design Masterclass
From absolute beginner to principal engineer, drawn step by step.
- 770 interactive lessons
- Step-by-step system design diagrams
- Live code editors
- Quizzes with instant feedback
- Progress tracking and streaks
- Lifetime access and all future lessons