Design an ATM & Cash Dispenser
In a 45-minute LLD interview, the interviewer evaluates two core architectural capabilities: lifecycle security via the State pattern (enforcing operations only when authenticated, ejecting on 3 failed PIN attempts) and atomic currency dispensing via Chain of Responsibility (two-phase plan/commit ensuring zero cassette inventory mutation on shortfall). Scope the solution to session lifecycle states, multi-denomination cascading (₹2000 → ₹500 → ₹100), explicit error handling via std::expected, and clean card session ejection. Mention cash deposits, Core Banking System (CBS) two-phase commit, and network timeout compensation as extensions.
Problem statement & clarifying requirements
Design an Automated Teller Machine (ATM) and Cash Dispenser. The system must manage a secure customer session lifecycle (Idle → CardInserted → PinVerified) while rejecting invalid actions at each stage. Cash withdrawal must be coordinated across currency denominations (₹2000, ₹500, ₹100) using a Chain of Responsibility that guarantees atomic rollback—if the ATM cannot satisfy the exact amount with its available notes, no notes may be deducted from the machine cassettes.
The card is retained/ejected for security, and the ATM transitions immediately back to IdleState.
Higher denominations (₹2000) are evaluated first to minimize note count, cascading down to ₹500 and ₹100.
The withdrawal transaction fails atomically. A two-phase plan-and-commit protocol ensures zero notes are deducted if the request cannot be completely satisfied.
Using C++23 std::expected<std::map<int, int>, std::string>, making domain failures explicit without relying on exception handling.
First, sketch the design yourself
Spend 5 minutes listing the classes, who owns whom, and which interface hides the part that changes.
- 1.
Model session transitions as discrete States: Avoid giant
switch(currentStatus)blocks inside every method. Each state (Idle,CardInserted,PinVerified) implements anAtmStateinterface and handles only valid operations. - 2.
Guarantee atomic currency dispensation: Never greedily deduct physical notes from cassettes during traversal. Run a planning phase that computes the denomination breakdown; only if the remainder reaches zero do you execute the commit phase.
- 3.
Separate hardware cassette inventory from banking cards: The ATM machine manages the
NoteDispenserchain, while theCardentity encapsulates customer credentials and account balance.
How to think through this problem (5-step walkthrough)
In a 45-minute LLD interview, the interviewer evaluates two core architectural capabilities: lifecycle security via the State pattern (enforcing operations only when authenticated, ejecting on 3 failed PIN attempts) and atomic currency dispensing via Chain of Responsibility (two-phase plan/commit ensuring zero cassette inventory mutation on shortfall). Scope the solution to session lifecycle states, multi-denomination cascading (₹2000 → ₹500 → ₹100), explicit error handling via std::expected, and clean card session ejection. Mention cash deposits, Core Banking System (CBS) two-phase commit, and network timeout compensation as extensions.
Not every noun in the prompt deserves its own class. Separate dumb data structs from stateful managers, and turn trivial nouns into fields or enums.
CardValue struct representing customer banking card credentials (card number, PIN hash, account balance).
AtmStateState pattern interface defining permitted operations for each lifecycle stage (insertCard, enterPin, withdrawCash, ejectCard).
AtmMachineContext entity holding the active state (std::unique_ptr<AtmState>), current card session, failed PIN attempt counter, and the note dispenser chain.
NoteDispenserChain of Responsibility handler managing a specific denomination cassette (e.g. ₹2000, ₹500, ₹100) and chaining to next_.
PinNumber / KeypadTrap noun — beginners often introduce separate PinNumber, Keypad, or CardSlot classes. A PIN is a simple std::string_view parameter passed into enterPin(), and the keypad/card-slot are hardware I/O interfaces that simply trigger ATM API calls.
A naive implementation relies on boolean flags (isCardInserted, isPinVerified) and massive switch (status) blocks across every method. Deducting notes greedily in a single loop (notes_[2000] -= count; notes_[500] -= count;) causes severe corruption: if the ATM has one ₹2000 note and requests ₹2100, the machine deducts the ₹2000 note, fails to find a ₹100 note, and either crashes or leaves the cassette ₹2000 lighter without giving the customer their money.
Scattering boolean authentication flags across all methods creates invalid states (e.g. withdrawing money without entering a PIN or inserting a card when a session is active).
A single monolithic dispensing loop cannot easily accommodate new cassettes or support atomic two-phase rollback when exact change is impossible.
Using raw boolean flags loses the reason for failure; throwing C++ exceptions for expected business rejections incurs stack-unwinding performance penalties.
- ✓
Two-phase atomic dispense invariant:
NoteDispenser::planDispense()computes the required note counts into a trial plan map without mutating cassette inventory;commitDispense()is invoked only if the remaining amount reaches exactly zero. - ✓
Security state invariant: Cash withdrawal is permitted exclusively in
PinVerifiedState. Three consecutive failed PIN attempts inCardInsertedStateimmediately eject the card and revert the machine toIdleState. - ✓
Cassette ownership invariant:
AtmMachineexclusively owns the head of the dispenser chain viastd::unique_ptr<NoteDispenser>, and each dispenser node exclusively owns its downstream successornext_.
Entities & design patterns used
Unlock Full UML Diagram & C++23 Solution
The complete UML diagram, production-ready C++23 code, follow-up extensions, and design pitfall analysis are part of Brute2Best Pro. All 7 foundation chapters are 100% free.
First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.