Course Content
Object-Oriented Design Interview
14 sections · 29 lessons
ATM: requirements, correctness under failure and the hardware model
Every earlier problem rewarded elegant modelling. The ATM rewards something else: correctness when a step fails halfway, because a wrong answer here moves real money in one direction or the other. This lesson covers the prompt, the requirements that cannot all be absolute at once, and a model built so that failures are representable — hardware behind interfaces and a durable log of every step.
Why this problem is scored differently
Every other problem in this course rewards elegant modelling. This one rewards correctness under failure, and it will trade elegance for it.
The reason is money. A parking lot that assigns a spot twice inconveniences a driver. An ATM that debits an account without dispensing has taken someone's money, and an ATM that dispenses without debiting has given away the bank's. Both are incidents with a paper trail and a regulator. So the interviewer will spend the round on failure paths, not on the class hierarchy, and a beautiful design with no answer for a jammed dispenser scores below an ordinary design that handles it.
The second thing this problem tests
Modelling the hardware as objects. An ATM is a card reader, a keypad, a screen, a cash dispenser, a printer, and a deposit slot, all in one cabinet. Candidates who treat these as implicit — calling some dispenseCash() utility — produce a design that cannot be tested and cannot represent a device failing. Candidates who model each device behind an interface get a design where "the printer is out of paper" is a representable state.
The five questions that change the model
1. "Which transactions — withdraw, deposit, balance, transfer, PIN change?" Each is a different flow. Withdrawal is the one with all the interesting failure modes; the others are variations. Assume withdraw, deposit, balance enquiry, and transfer, with withdrawal built properly and the rest as subtypes.
2. "Which denominations are stocked, and how many of each?" This is the setup for the dispensing algorithm in Cash dispensing, in code. Ask for the note values and whether the machine tracks its own counts. Assume ₹100, ₹200, and ₹500 notes with per-cassette counts.
3. "What happens on a network failure mid-transaction?" Ask this in minute two. It is the whole round, and asking it early signals you know that.
4. "Is the bank backend in scope, or is it a stubbed interface?" Take it as an interface: BankService.debit(account, amount, idempotencyKey). The interesting design is on this side of that boundary.
5. "One card slot, one user at a time?" Yes physically — but not one thread, which the section's extensions return to.
Assumptions this lesson makes
- A bank-owned ATM connected to a backend that can be slow or unreachable.
- Withdraw, deposit, balance, transfer. Cash withdrawal is built in full.
- Notes of ₹100, ₹200, and ₹500 in separate cassettes with tracked counts.
- Card plus PIN (personal identification number) authentication, three attempts, then the card is retained.
- The machine's own state survives a restart — a persistent transaction log exists.
Requirements
Functional requirements
1. Read a card and authenticate with a PIN, allowing three attempts2. Present a menu of transactions the account permits3. Withdraw: check limits, verify the machine can make the amount, debit, dispense4. Deposit: accept notes or an envelope, credit the account5. Balance enquiry and transfer between accounts6. Print a receipt, or offer to skip it7. Return the card — alwaysRequirement 7 looks trivial and is not. "Always return the card" is a rule with no exceptions short of a deliberate retention, and it must hold on every failure path — network down, notes jammed, power restored after a cut. Writing it as a numbered requirement is the right instinct.
Non-functional requirements
Three, and they are the design:
N1. Never dispense without debiting. The bank loses money.
N2. Never debit without dispensing. The customer loses money. Harder than N1, because the debit is a remote call and the dispense is a physical action that can fail after it succeeds.
N3. Always return the card unless it is deliberately retained. A swallowed card is a customer's day ruined and a support call.
You cannot have N1 and N2 absolutely at the same time, and being honest about that is the mark of a good answer. The physical dispense and the remote debit cannot be one atomic operation — there is no distributed transaction across a motor and a bank. What you can do is make every failure detectable and reversible:
"I can't make the debit and the dispense atomic — one is a network call and the other is a motor. What I can do is order them so that any failure is recoverable, log every step so a reversal is possible, and make the whole transaction idempotent so a retry cannot double-charge. That is the honest guarantee: not 'this never fails', but 'every failure is detectable and reversible'."
That paragraph, said in minute ten, is the strongest thing you can say on this problem.
What is cut
CUT (named, not forgotten)- The card network and issuer authorisation protocol (an interface)- Encryption and key management for the PIN pad (real, and a specialism)- Cheque deposit and image capture- The physical security, safe, and cash-in-transit process- Multi-language screens and accessibility modesSay the PIN encryption line carefully: "PIN handling in a real ATM uses hardware security modules and encrypted PIN blocks. I'll treat authentication as an interface and not pretend to design the cryptography." Knowing what you are not qualified to design in forty-five minutes is itself a signal.
Finding the objects
The candidate nouns
ATM, card, card reader, keypad, screen, printer, cash dispenser, cassette, deposit slot, account, customer, transaction, withdrawal, deposit, balance, PIN, receipt, bank.
Model the hardware, and why it changes everything
The move that makes this design good is treating each physical device as an object behind an interface:
1public interface CardReader {2 Optional<Card> read();3 void eject();4 void retain(); // swallow the card into the secure bin5 boolean isCardPresent();6}78public interface CashDispenser {9 void dispense(Map<Denomination, Integer> notes) throws DispenseFailedException;10 Map<Denomination, Integer> availableNotes();11 boolean isOperational();12}1314public interface ReceiptPrinter {15 void print(Receipt receipt) throws PrinterException;16 boolean hasPaper();17}1819public interface Screen { void display(String message); }20public interface Keypad { String readPin(); Money readAmount(); }Three concrete benefits, and naming them is what earns the point:
- The machine becomes testable. The whole withdrawal flow, including the jam, can be exercised with fake devices. Without this, nothing in the design can be tested without a physical machine.
- Device failures become representable.
hasPaper()returning false is a state the design can react to — offer to skip the receipt — rather than an exception from nowhere. - It is Interface Segregation (the SOLID reference) done for a real reason. One fat
Hardwareinterface would force the dispenser to implementprint.
Say it: "I'm modelling each device as its own interface. That is what makes the failure paths testable, and it means a jam is something the design can talk about rather than an exception that appears from nowhere."
Transactions as a hierarchy
A legitimate one-level hierarchy by the test in Inheritance, and its limits — a closed set of variants that differ in what they do:
1public abstract class Transaction {2 protected final String id; // also the idempotency key3 protected final Account account;4 protected final Instant startedAt;5 protected TransactionStatus status; // PENDING, COMPLETED, FAILED, REVERSED67 public abstract TransactionResult execute(ATMContext context);8}910public class WithdrawalTransaction extends Transaction { … }11public class DepositTransaction extends Transaction { … }12public class BalanceEnquiry extends Transaction { … }13public class TransferTransaction extends Transaction { … }The id doubling as the idempotency key is worth a sentence: every remote call carries it, so a retry after a timeout cannot debit twice. The section's extensions develop this.
The rest of the model
| Class | Its one-sentence job |
|---|---|
ATM | Holds the current state and the devices, and runs one session at a time |
ATMState | Decides what each event does in one phase of a session |
Session | One card's visit: the card, the authenticated account, and the transactions run |
Card | The card number, the account it maps to, and its status |
Account | Balance and limits — held by the bank, represented here as a snapshot |
Transaction (+ subtypes) | One operation the customer asked for |
CashInventory | What notes the machine holds, per denomination |
TransactionLog | The durable record of every step, for recovery and reversal |
BankService | The interface to the bank; can be slow, can fail |
TransactionLog is a class most attempts omit, and its absence is what makes recovery impossible. Put it on the diagram.