"Design the software for an ATM. Support card insertion, PIN authentication, balance inquiry, cash withdrawal, and deposit. Cash is dispensed as a mix of denominations."
B. Difficulty
Intermediate. The session flow (card → PIN → operation → dispense → eject) is a clean, bounded State machine; the two things that actually separate strong answers are denomination-splitting for cash dispensing and the balance-race that appears the instant more than one channel (a second ATM, or online banking) can touch the same account.
C. What This Problem Tests
Modeling the ATM session itself as a State machine with real illegal-transition guarantees (no withdrawal before PIN verification)
Chain of Responsibility for greedy denomination dispensing — a textbook fit that most candidates reach for a hand-rolled loop instead
Recognizing withdrawal as the same check-then-act balance race seen in every other resource-contention problem in this document, now with real money at stake
Correctly drawing the boundary between the ATM (a client) and the bank (the system of record for balances and authentication)
D. Requirements
Functional
Accept a card, prompt for and verify a PIN against the bank
Support balance inquiry, cash withdrawal, deposit, and transfer
Dispense withdrawn cash as an optimal mix of available denominations
Eject the card at the end of a session or on cancellation
Track the ATM's own cash inventory per denomination and refuse withdrawals it cannot physically fulfill
Non-Functional
A withdrawal must never debit an account without successfully dispensing the cash, and must never dispense cash without a successful debit — the two must be atomic as a pair
Concurrent withdrawal attempts against the same account (from this ATM or another channel) must not both succeed against an insufficient balance
E. Clarifying Questions
#
Question
Why It Matters
Design Decision Affected
1
Does the ATM own account/balance data, or is a separate bank system the source of truth?
Determines whether balance-check-and-debit is local or a remote call the ATM must treat carefully
Scope of BankServer boundary (see G, W)
2
Should the dispensing algorithm minimize the number of notes, or match a specific denomination preference?
Affects whether Chain of Responsibility order is greedy-largest-first or configurable
Order of DenominationHandler chain
3
Is this a single ATM, or a fleet reporting to a shared bank backend?
Determines whether cross-ATM/cross-channel concurrency is explicitly in scope
Scope of W
4
Are deposits credited immediately or held pending (e.g. check deposits)?
Affects whether deposit is a synchronous or asynchronous transaction
Assumption: the bank is a separate system of record reachable via a BankServer interface — the ATM is a client that authenticates and requests debits/credits, never the authority on an account's true balance.
Assumption: the ATM tracks its own physical cash inventory locally (it, not the bank, knows how many $20s are left in its own tray).
Assumption: transfer, if in scope, is treated as a withdrawal-equivalent debit paired with a credit to another account, reusing the same atomicity requirement.
G. Actors
Actor
Responsibility
Customer
Inserts card, enters PIN, selects a transaction
BankServer (external system)
Authenticates PIN, authorizes/executes the actual account debit/credit
ATMController (facade)
Drives the session state machine, coordinates dispensing
State (headline) for the session: a withdrawal action attempted from IdleState (no card yet) or CardInsertedState (PIN not yet verified) is structurally unavailable, not a runtime if (!authenticated) reject scattered across every transaction method.
Chain of Responsibility for dispensing: a CashDispenser for $270 hands the request to a Hundred-handler (dispenses two $100s, passes $70 down), then a Fifty-handler (dispenses one $50, passes $20 down), then a Twenty-handler (dispenses one $20, passes $0 down, chain stops). Each handler only knows its own denomination and how to pass a remainder along — a new denomination is a new link, not a change to a central dispensing algorithm.
Strategy for transaction type: withdrawal, deposit, balance inquiry, and transfer are mutually exclusive alternatives selected once per session — exactly Strategy's shape, resolved via a small Factory off the customer's menu selection, the same Factory-over-Strategy shape used for Rate Limiter's algorithm selection and Notification System's channel resolution.
Why the ATM is not the source of truth for balance: the ATM's own ATM class tracks only its physical cash tray; every debit/credit is delegated to BankServer, which is the actual system of record — this boundary is what makes the account-balance race in W a real, correctly-scoped concern rather than something the ATM could ever fix unilaterally.
State classes own session legality; dispenser chain owns cash math; transactions own bank interaction
One ATM god-class with a mode string field and every operation checking it inline
OCP
A new denomination is a new chain link; a new transaction type is a new Transaction implementation
A single dispense() method with a hardcoded denomination list that must be edited for a new note value
LSP
Every ATMState honestly rejects actions illegal for that phase rather than silently no-opping
OutOfServiceState.selectTransaction() quietly returning a default success instead of a clear rejection
ISP
Transaction and BankServer both stay small, focused interfaces
A single "AccountService" interface mixing authentication, balance, and physical-cash concerns
DIP
ATM depends on the BankServer interface, not a concrete bank integration
Hardcoding a specific bank's API calls directly inside WithdrawTransaction
M. Design Patterns
Pattern
Problem It Solves
Where It Appears
When NOT to Use
State
Session legality genuinely depends on phase, and illegal actions are a real correctness/safety risk
ATM / ATMState hierarchy
Skip only for a trivial single-operation kiosk with no session concept
Chain of Responsibility
Denomination splitting is a sequence of independent handlers, each contributing what it can and passing the remainder on
CashDispenser / DenominationHandler
Skip if the ATM only ever dispenses one fixed denomination
Strategy
Transaction type is a mutually exclusive choice made once per session
Transaction hierarchy
Skip if the ATM only supports one transaction type
Explicitly avoided: a hand-rolled greedy loop for dispensing inline in ATM — it works but re-implements exactly what Chain of Responsibility already models cleanly, and can't be extended with a new denomination without editing the loop itself.
N. What Is Likely to Change?
Behavior
Current Design
Anticipated Change
Protecting Abstraction
Denomination set
Fixed set of notes
New note denominations, coin dispensing
New DenominationHandler link, zero change to CashDispenser
Transaction types
Withdraw/deposit/balance/transfer
Bill pay, mobile top-up
New Transaction implementation
Auth method
Card + PIN
Card + biometric, cardless (phone) auth
BankServer.authenticate() signature generalized to an auth-context object rather than being tied to a physical card
O. Design Evolution
V1: one ATM class, a mode string, inline greedy dispensing loop.
V2: extract real ATMState classes once illegal-action rejection needs to be airtight, not just conditionally checked.
V3: extract the DenominationHandler chain once a second denomination or a "minimize note count" requirement makes the inline loop unwieldy.
V4: extract Transaction as Strategy once deposit/transfer are added alongside withdrawal.
V5: add the atomic debit-then-dispense sequencing (see W) once multi-channel access to the same account is in scope.
What stays stable: the ATMController's session API. New denominations and transaction types are pure additions.
P. API / Interface Design
interface ATMState {
void insertCard(Card card);
void enterPin(String pin);
void selectTransaction(Transaction t);
}
interface Transaction {
TransactionResult execute(Account account, BankServer bank);
}
class ATM {
void dispense(int amount); // delegates to CashDispenser chain
void setState(ATMState state);
}
class BankServer {
AuthResult authenticate(Card card, String pin);
DebitResult debit(Account account, int amount); // must be atomic at the bank
void credit(Account account, int amount);
}
interface ATMState {
void insertCard(Card card);
void enterPin(String pin);
void selectTransaction(Transaction t);
}
interface Transaction {
TransactionResult execute(Account account, BankServer bank);
}
class ATM {
void dispense(int amount); // delegates to CashDispenser chain
void setState(ATMState state);
}
class BankServer {
AuthResult authenticate(Card card, String pin);
DebitResult debit(Account account, int amount); // must be atomic at the bank
void credit(Account account, int amount);
}
class ATMState {
public:
virtual void insertCard(std::shared_ptr<Card> card) = 0;
virtual void enterPin(const std::string& pin) = 0;
virtual void selectTransaction(std::shared_ptr<Transaction> t) = 0;
};
class Transaction {
public:
virtual TransactionResult execute(Account& account, BankServer& bank) = 0;
};
class ATM {
public:
void dispense(int amount); // delegates to CashDispenser chain
void setState(std::shared_ptr<ATMState> state);
};
class BankServer {
public:
AuthResult authenticate(const Card& card, const std::string& pin);
DebitResult debit(Account& account, int amount); // must be atomic at the bank
void credit(Account& account, int amount);
};
class ATMState {
insertCard(card) { throw new Error('not implemented'); }
enterPin(pin) { throw new Error('not implemented'); }
selectTransaction(t) { throw new Error('not implemented'); }
}
class Transaction {
execute(account, bank) { throw new Error('not implemented'); }
}
class ATM {
dispense(amount) {} // delegates to CashDispenser chain
setState(state) {}
}
class BankServer {
authenticate(card, pin) {}
debit(account, amount) {} // must be atomic at the bank
credit(account, amount) {}
}
interface ATMState {
insertCard(card: Card): void;
enterPin(pin: string): void;
selectTransaction(t: Transaction): void;
}
interface Transaction {
execute(account: Account, bank: BankServer): TransactionResult;
}
class ATM {
dispense(amount: number): void {} // delegates to CashDispenser chain
setState(state: ATMState): void {}
}
class BankServer {
authenticate(card: Card, pin: string): AuthResult {}
debit(account: Account, amount: number): DebitResult {} // must be atomic at the bank
credit(account: Account, amount: number): void {}
}
Ownership/validation notes:WithdrawTransaction.execute() calls bank.debit()first and only calls atm.dispense() if the debit succeeds — never the reverse order, so a dispensing failure after a successful debit is a recoverable "credit back" case, while a dispensing-before-debit order would risk giving out cash with no guaranteed recovery if the debit then failed.
Q. Pseudocode — Critical Logic
WithdrawTransaction.execute(account, bank):
result = bank.debit(account, amount) // atomic at the bank: check-and-deduct as one op
if not result.success: return INSUFFICIENT_FUNDS
try:
atm.dispense(amount)
catch DispenseFailure:
bank.credit(account, amount) // compensating action — never leave a debit stranded
return DISPENSE_FAILED
return SUCCESS
DenominationHandler.handle(amount):
count = min(amount / this.value, this.availableCount)
dispense(count, this.value)
remainder = amount - (count * this.value)
if remainder > 0 and next != null: next.handle(remainder)
else if remainder > 0: throw InsufficientCashException
TransactionResult execute(Account account, BankServer bank) {
DebitResult result = bank.debit(account, amount); // atomic at the bank: check-and-deduct as one op
if (!result.isSuccess()) return TransactionResult.INSUFFICIENT_FUNDS;
try {
atm.dispense(amount);
} catch (DispenseFailureException e) {
bank.credit(account, amount); // compensating action — never leave a debit stranded
return TransactionResult.DISPENSE_FAILED;
}
return TransactionResult.SUCCESS;
}
void handle(int amount) throws InsufficientCashException {
int count = Math.min(amount / this.value, this.availableCount);
dispense(count, this.value);
int remainder = amount - (count * this.value);
if (remainder > 0 && next != null) next.handle(remainder);
else if (remainder > 0) throw new InsufficientCashException();
}
TransactionResult WithdrawTransaction::execute(Account& account, BankServer& bank) {
DebitResult result = bank.debit(account, amount); // atomic at the bank: check-and-deduct as one op
if (!result.success) return TransactionResult::INSUFFICIENT_FUNDS;
try {
atm.dispense(amount);
} catch (const DispenseFailure&) {
bank.credit(account, amount); // compensating action — never leave a debit stranded
return TransactionResult::DISPENSE_FAILED;
}
return TransactionResult::SUCCESS;
}
void DenominationHandler::handle(int amount) {
int count = std::min(amount / this->value, this->availableCount);
dispense(count, this->value);
int remainder = amount - (count * this->value);
if (remainder > 0 && next != nullptr) next->handle(remainder);
else if (remainder > 0) throw InsufficientCashException(); // throws InsufficientCashException
}
execute(account, bank) {
const result = bank.debit(account, this.amount); // atomic at the bank: check-and-deduct as one op
if (!result.success) return TransactionResult.INSUFFICIENT_FUNDS;
try {
atm.dispense(this.amount);
} catch (e) {
bank.credit(account, this.amount); // compensating action — never leave a debit stranded
return TransactionResult.DISPENSE_FAILED;
}
return TransactionResult.SUCCESS;
}
handle(amount) {
const count = Math.min(Math.floor(amount / this.value), this.availableCount);
dispense(count, this.value);
const remainder = amount - (count * this.value);
if (remainder > 0 && this.next != null) this.next.handle(remainder);
else if (remainder > 0) throw new Error('InsufficientCashException');
}
execute(account: Account, bank: BankServer): TransactionResult {
const result = bank.debit(account, this.amount); // atomic at the bank: check-and-deduct as one op
if (!result.success) return TransactionResult.INSUFFICIENT_FUNDS;
try {
atm.dispense(this.amount);
} catch (e) {
bank.credit(account, this.amount); // compensating action — never leave a debit stranded
return TransactionResult.DISPENSE_FAILED;
}
return TransactionResult.SUCCESS;
}
handle(amount: number): void {
const count = Math.min(Math.floor(amount / this.value), this.availableCount);
dispense(count, this.value);
const remainder = amount - (count * this.value);
if (remainder > 0 && this.next != null) this.next.handle(remainder);
else if (remainder > 0) throw new Error('InsufficientCashException');
}
Linked chain of DenominationHandler, ordered largest-to-smallest
Greedy denomination splitting
Minimizes note count for the common case; each handler only needs to know its own value and successor
A DP-based "minimum notes" solver handles edge cases greedy can miss (e.g. certain denomination sets), but real ATMs use greedy because their denomination sets are chosen to make greedy optimal — worth naming both, defaulting to greedy
Map<Denomination, Integer> for tray inventory
Tracking remaining notes per denomination
O(1) availability check per handler
—
T. Complexity Analysis
Operation
Time
Space
Dispensing (chain traversal)
O(number of denominations) — a small constant
O(1)
Authentication / debit
O(1) local, dominated by the network round-trip to BankServer
O(1)
U. Testing Strategy
Happy path: a withdrawal amount is split into the fewest notes possible given current tray inventory.
Edge cases: a withdrawal exceeding the ATM's total physical cash is rejected before any debit occurs; a withdrawal amount not representable by available denominations (e.g. $15 with only $20/$50/$100 in the tray) is rejected cleanly.
Atomicity tests: a simulated dispense failure after a successful debit triggers the compensating credit — the account never ends up debited with no cash received.
Concurrency tests: two simultaneous withdrawal attempts against an account with only enough balance for one must result in exactly one success and one clean rejection, never both succeeding.
V. Interviewer Follow-Ups
Follow-Up
Testing
Design Change
Classes Changed
Pattern/Trade-off
"What if the ATM runs out of a denomination mid-dispense?"
Chain-of-Responsibility failure handling
The failing handler throws; already-dispensed notes must be tracked for reconciliation, and the transaction compensates via bank.credit()
DenominationHandler, WithdrawTransaction
Directly exercises Q/Z
"Two ATMs, same account, same instant, both requesting the full balance."
The check-then-act balance race, once more
None if bank.debit() is already atomic at the bank; candidate should trace why it must be (compare-and-deduct as one operation, not read-then-write)
N/A — validates W
The account-balance instance of this document's most repeated concurrency lesson
"Support cardless, phone-based authentication."
Whether auth was tied unnecessarily to a physical card
Generalize authenticate()'s input to an auth-context abstraction
BankServer interface, ATMState entry point
Tests interface generality
W. Concurrency
Shared mutable state: the account balance at the bank (shared across every ATM and every other channel touching that account); the ATM's own local cash tray (shared only across concurrent sessions on that one machine, a much smaller-scoped concern).
The check-then-act balance race, once more: "read balance, confirm sufficient, then debit" as two separate steps is exactly the same bug shape as Parking Lot's spot occupation and Splitwise's ledger writes, now scoped to money. bank.debit() must be a single atomic compare-and-deduct operation at the bank (a conditional update, not read-then-write), so two concurrent withdrawal requests against a balance that can satisfy only one of them result in exactly one success.
The ATM's local cash tray is a much smaller-scoped lock — it only needs to serialize concurrent sessions on the same physical machine, and is irrelevant to the cross-channel account race above.
Debit-then-dispense ordering (see P/Q) is what keeps a partial failure recoverable — the compensating credit only has to reverse a debit, never has to "claw back" cash already physically dispensed.
X. Persistence
The ATM itself is stateless across sessions beyond its physical cash inventory — every account/transaction fact of record lives at the bank. Locally, only a session-level transaction log (for reconciliation and dispute resolution) needs to persist, behind a simple repository interface, independent of ATMState or BankServer.
Y. Scaling Boundary
Layer
What Belongs Here
LLD
Session state machine, denomination dispensing chain, transaction Strategy, debit/dispense ordering
The bank's internal balance-storage architecture — the ATM only needs BankServer to guarantee atomicity, not to design that system itself
Z. Failure Scenarios
Network failure between debit success and dispense confirmation: without a clear acknowledgment, this is the same ambiguous-outcome problem every payment system faces — the design should favor a reconciliation job that reviews debits with no matching dispense record, rather than assuming success or blindly retrying (which could double-dispense).
Card left in the machine / customer walks away mid-session: a timeout transitions any active state back toward IdleState and ejects the card automatically, rather than holding the session open indefinitely.
Tray physically empty for a denomination the chain expected: the handler's availableCount check (not just the logical count from the last known inventory) is what's authoritative — a hardware-reported count, not an assumed one.
AA. Common Bad Designs
One ATM god-class with a mode field and every method starting with an if (mode != AUTHENTICATED) return guard — the State-machine violation seen across this entire document.
A hand-rolled greedy dispensing loop inline in the withdrawal method instead of Chain of Responsibility — works, but can't add a denomination without touching the loop.
Dispensing cash before confirming the debit succeeded — the reverse of the safe ordering in Q, and the harder failure mode to recover from.
Treating the ATM's local balance cache as authoritative instead of always deferring to BankServer — reintroduces the exact race W exists to prevent.
AB. Design Trade-Offs
Decision
Option A
Option B
Recommendation
Dispensing algorithm
Greedy largest-denomination-first (Chain of Responsibility)
DP-based minimum-notes solver
Greedy — real ATM denomination sets are chosen specifically to make greedy optimal, and it's dramatically simpler to implement and reason about
Debit/dispense ordering
Debit first, then dispense, with compensating credit on dispense failure
Dispense first, then debit
Debit first — a failed compensating credit is a bank-side accounting fix; a failure after cash has already left the machine is unrecoverable
AC. Interviewer Evaluation
Strong signal: proposing State for the session unprompted; reaching for Chain of Responsibility for dispensing rather than a hand-rolled loop; independently naming the debit-before-dispense ordering and the compensating-credit recovery path; recognizing the balance race as the same shape already seen in Parking Lot/Splitwise.
Red flag: a mode-flag ATM class; a hardcoded dispensing loop with no extension point; dispensing before confirming the debit; treating the ATM as the account's source of truth.
AD. Interview Simulation
Interviewer: Design an ATM.
Candidate: The session itself is a State machine — idle, card-inserted, authenticated, dispensing, out-of-service — so a withdrawal attempted before PIN verification is structurally impossible, not just conditionally rejected.
Interviewer: How do you dispense $270 as physical notes?
Candidate: Chain of Responsibility — a $100 handler takes as many $100s as it can and passes the remainder to the $50 handler, and so on down to $1. Adding a new denomination is a new link in the chain, not a change to a central dispensing method.
Interviewer: What if someone tries to withdraw from two ATMs at once with just enough balance for one withdrawal?
Candidate: That's the same check-then-act race as a parking spot or a shared ledger — the bank's debit has to be one atomic compare-and-deduct operation, not read-balance-then-write-balance as two steps, or both ATMs could see a sufficient balance and both succeed.
Interviewer: What if the debit succeeds but the cash fails to dispense?
Candidate: That's exactly why I debit before dispensing rather than the reverse — a dispense failure after a successful debit is recoverable with a compensating credit back to the account, whereas a debit failure after cash has already left the machine would not be recoverable at all.
Don't design the bank's internal ledger/distributed-systems architecture — the ATM only needs it to be atomic
AG. Full Implementation
A complete, compilable reference implementation of every class in the diagram above (composition, inheritance, interface realization, and dependency-injected strategies all preserved per section I), with a runnable demo exercising the primary flow — in Java, C++, JavaScript, and TypeScript.
// ATM Machine - full LLD implementation (single-file demo).
// No "Concurrency" badge -> single-threaded; the "must be atomic" requirement is
// modeled as BankServer.debit() being a single check-and-deduct call, not via locks.
//
// Design notes:
// - State pattern for the session: withdrawing from IdleState/CardInsertedState is
// structurally unavailable (default no-op / rejection), not a scattered runtime if.
// - Chain of Responsibility for CashDispenser: each DenominationHandler only knows its
// own denomination and passes a remainder to the next link - a new bill is a new
// link, not a change to a central dispensing algorithm.
// - Strategy for Transaction: withdrawal/deposit/balance/transfer are mutually
// exclusive alternatives selected once per session.
// - BankServer is the system of record for balance; ATM only tracks its own physical
// cash tray - this is why WithdrawTransaction debits first, then dispenses, with a
// compensating credit() if dispensing fails.
import java.util.*;
public class p14 {
// ---------- Supporting domain types ----------
static class Card {
final String cardNumber;
Card(String cardNumber) { this.cardNumber = cardNumber; }
}
static class Account {
final String accountId;
double balance;
Account(String accountId, double balance) { this.accountId = accountId; this.balance = balance; }
}
enum AuthResult { SUCCESS, INVALID_PIN }
enum DebitStatus { SUCCESS, INSUFFICIENT_FUNDS }
static class DebitResult {
final DebitStatus status;
DebitResult(DebitStatus status) { this.status = status; }
}
enum TransactionResult { SUCCESS, INSUFFICIENT_FUNDS, DISPENSE_FAILED, BAD_AMOUNT }
// ---------- BankServer (interface, external authority) ----------
interface BankServer {
AuthResult authenticate(Card card, String pin);
Account accountFor(Card card);
DebitResult debit(Account account, int amount); // atomic check-and-deduct at the bank
void credit(Account account, int amount);
}
// In-memory stand-in for "the bank" - the actual system of record for balance.
static class SimpleBankServer implements BankServer {
private final Map<String, String> pins = new HashMap<>();
private final Map<String, Account> accountsByCard = new HashMap<>();
void enroll(Card card, String pin, Account account) {
pins.put(card.cardNumber, pin);
accountsByCard.put(card.cardNumber, account);
}
public AuthResult authenticate(Card card, String pin) {
return pin.equals(pins.get(card.cardNumber)) ? AuthResult.SUCCESS : AuthResult.INVALID_PIN;
}
public Account accountFor(Card card) { return accountsByCard.get(card.cardNumber); }
// Atomic check-and-deduct: never leaves the account debited below zero.
public synchronized DebitResult debit(Account account, int amount) {
if (account.balance < amount) return new DebitResult(DebitStatus.INSUFFICIENT_FUNDS);
account.balance -= amount;
return new DebitResult(DebitStatus.SUCCESS);
}
public synchronized void credit(Account account, int amount) { account.balance += amount; }
}
// ---------- CashDispenser / DenominationHandler (Chain of Responsibility) ----------
static class InsufficientCashException extends RuntimeException {
InsufficientCashException(String msg) { super(msg); }
}
static abstract class DenominationHandler {
final int value;
int count;
DenominationHandler next;
DenominationHandler(int value, int count) { this.value = value; this.count = count; }
void setNext(DenominationHandler next) { this.next = next; }
// Greedily contributes as many of its own bills as it can, then passes the
// remainder down the chain. Only knows its own denomination.
void handle(int amount) {
int used = Math.min(amount / value, count);
if (used > 0) {
count -= used;
System.out.println(" dispensing " + used + " x $" + value);
}
int remainder = amount - used * value;
if (remainder > 0 && next != null) next.handle(remainder);
else if (remainder > 0) throw new InsufficientCashException("Cannot make change for remainder $" + remainder);
}
}
static class TwentyHandler extends DenominationHandler { TwentyHandler(int count) { super(20, count); } }
static class FiftyHandler extends DenominationHandler { FiftyHandler(int count) { super(50, count); } }
static class HundredHandler extends DenominationHandler { HundredHandler(int count) { super(100, count); } }
static class CashDispenser {
private final DenominationHandler chainHead;
CashDispenser(DenominationHandler chainHead) { this.chainHead = chainHead; }
void dispense(int amount) { chainHead.handle(amount); }
}
// ---------- Transaction (interface, Strategy) ----------
interface Transaction {
TransactionResult execute(Account account, BankServer bank, ATM atm);
}
static class WithdrawTransaction implements Transaction {
final int amount;
WithdrawTransaction(int amount) { this.amount = amount; }
public TransactionResult execute(Account account, BankServer bank, ATM atm) {
if (amount <= 0) return TransactionResult.BAD_AMOUNT;
DebitResult result = bank.debit(account, amount); // atomic at the bank
if (result.status != DebitStatus.SUCCESS) return TransactionResult.INSUFFICIENT_FUNDS;
try {
atm.dispense(amount);
} catch (InsufficientCashException e) {
bank.credit(account, amount); // compensating action - never leave a debit stranded
return TransactionResult.DISPENSE_FAILED;
}
return TransactionResult.SUCCESS;
}
}
static class DepositTransaction implements Transaction {
final int amount;
DepositTransaction(int amount) { this.amount = amount; }
public TransactionResult execute(Account account, BankServer bank, ATM atm) {
if (amount <= 0) return TransactionResult.BAD_AMOUNT;
bank.credit(account, amount);
return TransactionResult.SUCCESS;
}
}
static class BalanceInquiryTransaction implements Transaction {
public TransactionResult execute(Account account, BankServer bank, ATM atm) {
System.out.println(" Balance for " + account.accountId + ": $" + account.balance);
return TransactionResult.SUCCESS;
}
}
static class TransferTransaction implements Transaction {
final Account destination;
final int amount;
TransferTransaction(Account destination, int amount) { this.destination = destination; this.amount = amount; }
public TransactionResult execute(Account account, BankServer bank, ATM atm) {
DebitResult result = bank.debit(account, amount);
if (result.status != DebitStatus.SUCCESS) return TransactionResult.INSUFFICIENT_FUNDS;
bank.credit(destination, amount);
return TransactionResult.SUCCESS;
}
}
// ---------- ATMState (interface, State) ----------
interface ATMState {
void insertCard(Card card);
void enterPin(String pin);
void selectTransaction(Transaction t);
void ejectCard();
String name();
}
static class IdleState implements ATMState {
final ATM atm;
IdleState(ATM atm) { this.atm = atm; }
public void insertCard(Card card) {
atm.currentCard = card;
atm.setState(atm.cardInsertedState);
}
public void enterPin(String pin) { System.out.println("Reject: insert a card first"); }
public void selectTransaction(Transaction t) { System.out.println("Reject: insert a card first"); }
public void ejectCard() { System.out.println("Reject: no card to eject"); }
public String name() { return "Idle"; }
}
static class CardInsertedState implements ATMState {
final ATM atm;
CardInsertedState(ATM atm) { this.atm = atm; }
public void insertCard(Card card) { System.out.println("Reject: a card is already inserted"); }
public void enterPin(String pin) {
AuthResult result = atm.bank.authenticate(atm.currentCard, pin);
if (result == AuthResult.SUCCESS) {
atm.currentAccount = atm.bank.accountFor(atm.currentCard);
atm.setState(atm.authenticatedState);
} else {
System.out.println("Reject: invalid PIN");
atm.setState(atm.idleState);
atm.currentCard = null;
}
}
public void selectTransaction(Transaction t) { System.out.println("Reject: PIN not verified yet"); }
public void ejectCard() { atm.currentCard = null; atm.setState(atm.idleState); }
public String name() { return "CardInserted"; }
}
static class AuthenticatedState implements ATMState {
final ATM atm;
AuthenticatedState(ATM atm) { this.atm = atm; }
public void insertCard(Card card) { System.out.println("Reject: session already active"); }
public void enterPin(String pin) { System.out.println("Reject: already authenticated"); }
public void selectTransaction(Transaction t) {
atm.setState(atm.dispensingState);
TransactionResult result = t.execute(atm.currentAccount, atm.bank, atm);
System.out.println(" Transaction result: " + result);
atm.setState(atm.authenticatedState); // back to menu for another transaction, or eject
}
public void ejectCard() { atm.currentCard = null; atm.currentAccount = null; atm.setState(atm.idleState); }
public String name() { return "Authenticated"; }
}
// Transient state while a withdrawal's cash is physically moving; kept as its own
// state so an in-progress dispense is a distinct, non-reentrant phase.
static class DispensingState implements ATMState {
final ATM atm;
DispensingState(ATM atm) { this.atm = atm; }
public void insertCard(Card card) { System.out.println("Reject: dispensing in progress"); }
public void enterPin(String pin) { System.out.println("Reject: dispensing in progress"); }
public void selectTransaction(Transaction t) { System.out.println("Reject: dispensing in progress"); }
public void ejectCard() { System.out.println("Reject: dispensing in progress"); }
public String name() { return "Dispensing"; }
}
static class OutOfServiceState implements ATMState {
public void insertCard(Card card) { System.out.println("ATM out of service"); }
public void enterPin(String pin) { System.out.println("ATM out of service"); }
public void selectTransaction(Transaction t) { System.out.println("ATM out of service"); }
public void ejectCard() { System.out.println("ATM out of service"); }
public String name() { return "OutOfService"; }
}
// ---------- ATM (context) ----------
static class ATM {
ATMState currentState;
final IdleState idleState = new IdleState(this);
final CardInsertedState cardInsertedState = new CardInsertedState(this);
final AuthenticatedState authenticatedState = new AuthenticatedState(this);
final DispensingState dispensingState = new DispensingState(this);
final OutOfServiceState outOfServiceState = new OutOfServiceState();
final BankServer bank;
final CashDispenser cashDispenser;
Card currentCard;
Account currentAccount;
ATM(BankServer bank, CashDispenser cashDispenser) {
this.bank = bank;
this.cashDispenser = cashDispenser;
this.currentState = idleState;
}
void setState(ATMState state) { this.currentState = state; }
void dispense(int amount) { cashDispenser.dispense(amount); } // delegates to the chain
void insertCard(Card card) { currentState.insertCard(card); }
void enterPin(String pin) { currentState.enterPin(pin); }
void selectTransaction(Transaction t) { currentState.selectTransaction(t); }
void ejectCard() { currentState.ejectCard(); }
}
// ---------- Demo ----------
public static void main(String[] args) {
SimpleBankServer bank = new SimpleBankServer();
Card card = new Card("4111-CARD");
Account account = new Account("ACC-1", 500);
Account friendAccount = new Account("ACC-2", 0);
bank.enroll(card, "1234", account);
HundredHandler hundreds = new HundredHandler(3);
FiftyHandler fifties = new FiftyHandler(2);
TwentyHandler twenties = new TwentyHandler(5);
hundreds.setNext(fifties);
fifties.setNext(twenties);
CashDispenser dispenser = new CashDispenser(hundreds);
ATM atm = new ATM(bank, dispenser);
atm.insertCard(card);
System.out.println("State: " + atm.currentState.name());
atm.enterPin("0000"); // wrong PIN -> ejects back to idle
System.out.println("State after bad PIN: " + atm.currentState.name());
atm.insertCard(card);
atm.enterPin("1234"); // correct PIN
System.out.println("State: " + atm.currentState.name());
atm.selectTransaction(new BalanceInquiryTransaction());
atm.selectTransaction(new WithdrawTransaction(170)); // 1x100 + 1x50 + 1x20
atm.selectTransaction(new BalanceInquiryTransaction());
atm.selectTransaction(new TransferTransaction(friendAccount, 100));
System.out.println(" Friend balance: $" + friendAccount.balance);
atm.selectTransaction(new DepositTransaction(500)); // top up so the next withdrawal fails on CASH, not funds
atm.selectTransaction(new WithdrawTransaction(300)); // exceeds remaining cash on hand -> compensating credit
atm.selectTransaction(new BalanceInquiryTransaction());
atm.ejectCard();
System.out.println("State: " + atm.currentState.name());
}
}
// ATM Machine - full LLD implementation (single-file demo).
// No "Concurrency" badge -> single-threaded; the "must be atomic" requirement is
// modeled as BankServer::debit() being a single check-and-deduct call, not via mutexes.
//
// Design notes:
// - State pattern for the session: withdrawing from IdleState/CardInsertedState is
// structurally unavailable (default rejection), not a scattered runtime if.
// - Chain of Responsibility for CashDispenser: each DenominationHandler only knows its
// own denomination and passes a remainder to the next link.
// - Strategy for Transaction: withdrawal/deposit/balance/transfer are mutually
// exclusive alternatives selected once per session.
// - BankServer is the system of record for balance; ATM only tracks its physical cash
// tray - hence WithdrawTransaction debits first, then dispenses, with a compensating
// credit() if dispensing fails.
#include <iostream>
#include <string>
#include <unordered_map>
#include <memory>
#include <stdexcept>
using namespace std;
// ---------- Supporting domain types ----------
struct Card { string cardNumber; };
struct Account {
string accountId;
double balance;
};
enum class AuthResult { SUCCESS, INVALID_PIN };
enum class DebitStatus { SUCCESS, INSUFFICIENT_FUNDS };
struct DebitResult { DebitStatus status; };
enum class TransactionResult { SUCCESS, INSUFFICIENT_FUNDS, DISPENSE_FAILED, BAD_AMOUNT };
static string toString(TransactionResult r) {
switch (r) {
case TransactionResult::SUCCESS: return "SUCCESS";
case TransactionResult::INSUFFICIENT_FUNDS: return "INSUFFICIENT_FUNDS";
case TransactionResult::DISPENSE_FAILED: return "DISPENSE_FAILED";
case TransactionResult::BAD_AMOUNT: return "BAD_AMOUNT";
}
return "?";
}
class ATM; // fwd decl
// ---------- BankServer (interface, external authority) ----------
class BankServer {
public:
virtual ~BankServer() = default;
virtual AuthResult authenticate(const Card& card, const string& pin) = 0;
virtual Account* accountFor(const Card& card) = 0;
virtual DebitResult debit(Account& account, int amount) = 0; // atomic at the bank
virtual void credit(Account& account, int amount) = 0;
};
class SimpleBankServer : public BankServer {
unordered_map<string, string> pins;
unordered_map<string, Account*> accountsByCard;
public:
void enroll(const Card& card, const string& pin, Account* account) {
pins[card.cardNumber] = pin;
accountsByCard[card.cardNumber] = account;
}
AuthResult authenticate(const Card& card, const string& pin) override {
auto it = pins.find(card.cardNumber);
return (it != pins.end() && it->second == pin) ? AuthResult::SUCCESS : AuthResult::INVALID_PIN;
}
Account* accountFor(const Card& card) override { return accountsByCard.at(card.cardNumber); }
// Atomic check-and-deduct: never leaves the account debited below zero.
DebitResult debit(Account& account, int amount) override {
if (account.balance < amount) return { DebitStatus::INSUFFICIENT_FUNDS };
account.balance -= amount;
return { DebitStatus::SUCCESS };
}
void credit(Account& account, int amount) override { account.balance += amount; }
};
// ---------- CashDispenser / DenominationHandler (Chain of Responsibility) ----------
struct InsufficientCashException : public runtime_error {
InsufficientCashException(const string& msg) : runtime_error(msg) {}
};
class DenominationHandler {
public:
int value;
int count;
DenominationHandler* next = nullptr;
DenominationHandler(int value, int count) : value(value), count(count) {}
virtual ~DenominationHandler() = default;
void setNext(DenominationHandler* n) { next = n; }
// Greedily contributes as many of its own bills as possible, passes the remainder on.
void handle(int amount) {
int used = min(amount / value, count);
if (used > 0) {
count -= used;
cout << " dispensing " << used << " x $" << value << "\n";
}
int remainder = amount - used * value;
if (remainder > 0 && next != nullptr) next->handle(remainder);
else if (remainder > 0) throw InsufficientCashException("Cannot make change for remainder $" + to_string(remainder));
}
};
class TwentyHandler : public DenominationHandler { public: TwentyHandler(int c) : DenominationHandler(20, c) {} };
class FiftyHandler : public DenominationHandler { public: FiftyHandler(int c) : DenominationHandler(50, c) {} };
class HundredHandler : public DenominationHandler { public: HundredHandler(int c) : DenominationHandler(100, c) {} };
class CashDispenser {
DenominationHandler* chainHead;
public:
explicit CashDispenser(DenominationHandler* chainHead) : chainHead(chainHead) {}
void dispense(int amount) { chainHead->handle(amount); }
};
// ---------- Transaction (interface, Strategy) ----------
class Transaction {
public:
virtual ~Transaction() = default;
virtual TransactionResult execute(Account& account, BankServer& bank, ATM& atm) = 0;
};
class WithdrawTransaction : public Transaction {
int amount;
public:
explicit WithdrawTransaction(int amount) : amount(amount) {}
TransactionResult execute(Account& account, BankServer& bank, ATM& atm) override;
};
class DepositTransaction : public Transaction {
int amount;
public:
explicit DepositTransaction(int amount) : amount(amount) {}
TransactionResult execute(Account& account, BankServer& bank, ATM&) override {
if (amount <= 0) return TransactionResult::BAD_AMOUNT;
bank.credit(account, amount);
return TransactionResult::SUCCESS;
}
};
class BalanceInquiryTransaction : public Transaction {
public:
TransactionResult execute(Account& account, BankServer&, ATM&) override {
cout << " Balance for " << account.accountId << ": $" << account.balance << "\n";
return TransactionResult::SUCCESS;
}
};
class TransferTransaction : public Transaction {
Account* destination;
int amount;
public:
TransferTransaction(Account* destination, int amount) : destination(destination), amount(amount) {}
TransactionResult execute(Account& account, BankServer& bank, ATM&) override {
DebitResult result = bank.debit(account, amount);
if (result.status != DebitStatus::SUCCESS) return TransactionResult::INSUFFICIENT_FUNDS;
bank.credit(*destination, amount);
return TransactionResult::SUCCESS;
}
};
// ---------- ATMState (interface, State) ----------
class ATMState {
public:
virtual ~ATMState() = default;
virtual void insertCard(const Card& card) = 0;
virtual void enterPin(const string& pin) = 0;
virtual void selectTransaction(Transaction& t) = 0;
virtual void ejectCard() = 0;
virtual string name() = 0;
};
// ---------- ATM (context) ----------
class ATM {
public:
ATMState* currentState;
unique_ptr<ATMState> idleState, cardInsertedState, authenticatedState, dispensingState, outOfServiceState;
BankServer& bank;
CashDispenser& cashDispenser;
Card currentCard;
bool hasCard = false;
Account* currentAccount = nullptr;
ATM(BankServer& bank, CashDispenser& dispenser);
void setState(ATMState* state) { currentState = state; }
void dispense(int amount) { cashDispenser.dispense(amount); } // delegates to the chain
void insertCard(const Card& card) { currentState->insertCard(card); }
void enterPin(const string& pin) { currentState->enterPin(pin); }
void selectTransaction(Transaction& t) { currentState->selectTransaction(t); }
void ejectCard() { currentState->ejectCard(); }
};
TransactionResult WithdrawTransaction::execute(Account& account, BankServer& bank, ATM& atm) {
if (amount <= 0) return TransactionResult::BAD_AMOUNT;
DebitResult result = bank.debit(account, amount); // atomic at the bank
if (result.status != DebitStatus::SUCCESS) return TransactionResult::INSUFFICIENT_FUNDS;
try {
atm.dispense(amount);
} catch (const InsufficientCashException&) {
bank.credit(account, amount); // compensating action - never leave a debit stranded
return TransactionResult::DISPENSE_FAILED;
}
return TransactionResult::SUCCESS;
}
class IdleStateImpl : public ATMState {
ATM& atm;
public:
explicit IdleStateImpl(ATM& atm) : atm(atm) {}
void insertCard(const Card& card) override;
void enterPin(const string&) override { cout << "Reject: insert a card first\n"; }
void selectTransaction(Transaction&) override { cout << "Reject: insert a card first\n"; }
void ejectCard() override { cout << "Reject: no card to eject\n"; }
string name() override { return "Idle"; }
};
class CardInsertedStateImpl : public ATMState {
ATM& atm;
public:
explicit CardInsertedStateImpl(ATM& atm) : atm(atm) {}
void insertCard(const Card&) override { cout << "Reject: a card is already inserted\n"; }
void enterPin(const string& pin) override;
void selectTransaction(Transaction&) override { cout << "Reject: PIN not verified yet\n"; }
void ejectCard() override { atm.hasCard = false; atm.setState(atm.idleState.get()); }
string name() override { return "CardInserted"; }
};
class AuthenticatedStateImpl : public ATMState {
ATM& atm;
public:
explicit AuthenticatedStateImpl(ATM& atm) : atm(atm) {}
void insertCard(const Card&) override { cout << "Reject: session already active\n"; }
void enterPin(const string&) override { cout << "Reject: already authenticated\n"; }
void selectTransaction(Transaction& t) override {
atm.setState(atm.dispensingState.get());
TransactionResult result = t.execute(*atm.currentAccount, atm.bank, atm);
cout << " Transaction result: " << toString(result) << "\n";
atm.setState(atm.authenticatedState.get()); // back to menu, or eject
}
void ejectCard() override { atm.hasCard = false; atm.currentAccount = nullptr; atm.setState(atm.idleState.get()); }
string name() override { return "Authenticated"; }
};
// Transient state while cash is physically moving - a distinct, non-reentrant phase.
class DispensingStateImpl : public ATMState {
public:
void insertCard(const Card&) override { cout << "Reject: dispensing in progress\n"; }
void enterPin(const string&) override { cout << "Reject: dispensing in progress\n"; }
void selectTransaction(Transaction&) override { cout << "Reject: dispensing in progress\n"; }
void ejectCard() override { cout << "Reject: dispensing in progress\n"; }
string name() override { return "Dispensing"; }
};
class OutOfServiceStateImpl : public ATMState {
public:
void insertCard(const Card&) override { cout << "ATM out of service\n"; }
void enterPin(const string&) override { cout << "ATM out of service\n"; }
void selectTransaction(Transaction&) override { cout << "ATM out of service\n"; }
void ejectCard() override { cout << "ATM out of service\n"; }
string name() override { return "OutOfService"; }
};
void IdleStateImpl::insertCard(const Card& card) {
atm.currentCard = card;
atm.hasCard = true;
atm.setState(atm.cardInsertedState.get());
}
void CardInsertedStateImpl::enterPin(const string& pin) {
AuthResult result = atm.bank.authenticate(atm.currentCard, pin);
if (result == AuthResult::SUCCESS) {
atm.currentAccount = atm.bank.accountFor(atm.currentCard);
atm.setState(atm.authenticatedState.get());
} else {
cout << "Reject: invalid PIN\n";
atm.setState(atm.idleState.get());
atm.hasCard = false;
}
}
ATM::ATM(BankServer& bank, CashDispenser& dispenser) : bank(bank), cashDispenser(dispenser) {
idleState = make_unique<IdleStateImpl>(*this);
cardInsertedState = make_unique<CardInsertedStateImpl>(*this);
authenticatedState = make_unique<AuthenticatedStateImpl>(*this);
dispensingState = make_unique<DispensingStateImpl>();
outOfServiceState = make_unique<OutOfServiceStateImpl>();
currentState = idleState.get();
}
// ---------- Demo ----------
int main() {
SimpleBankServer bank;
Card card{"4111-CARD"};
Account account{"ACC-1", 500};
Account friendAccount{"ACC-2", 0};
bank.enroll(card, "1234", &account);
HundredHandler hundreds(3);
FiftyHandler fifties(2);
TwentyHandler twenties(5);
hundreds.setNext(&fifties);
fifties.setNext(&twenties);
CashDispenser dispenser(&hundreds);
ATM atm(bank, dispenser);
atm.insertCard(card);
cout << "State: " << atm.currentState->name() << "\n";
atm.enterPin("0000"); // wrong PIN -> ejects back to idle
cout << "State after bad PIN: " << atm.currentState->name() << "\n";
atm.insertCard(card);
atm.enterPin("1234"); // correct PIN
cout << "State: " << atm.currentState->name() << "\n";
BalanceInquiryTransaction balanceTxn;
atm.selectTransaction(balanceTxn);
WithdrawTransaction withdraw170(170); // 1x100 + 1x50 + 1x20
atm.selectTransaction(withdraw170);
atm.selectTransaction(balanceTxn);
TransferTransaction transfer(&friendAccount, 100);
atm.selectTransaction(transfer);
cout << " Friend balance: $" << friendAccount.balance << "\n";
DepositTransaction deposit500(500);
atm.selectTransaction(deposit500);
WithdrawTransaction withdraw300(300); // exceeds remaining cash on hand -> compensating credit
atm.selectTransaction(withdraw300);
atm.selectTransaction(balanceTxn);
atm.ejectCard();
cout << "State: " << atm.currentState->name() << "\n";
return 0;
}
// ATM Machine - full LLD implementation (single-file demo, Node/CommonJS).
// No "Concurrency" badge -> single-threaded; the "must be atomic" requirement is
// modeled as BankServer.debit() being a single check-and-deduct call, not via locks.
//
// Design notes:
// - State pattern for the session: withdrawing from IdleState/CardInsertedState is
// structurally unavailable (default rejection), not a scattered runtime if.
// - Chain of Responsibility for CashDispenser: each DenominationHandler only knows its
// own denomination and passes a remainder to the next link.
// - Strategy for Transaction: withdrawal/deposit/balance/transfer are mutually
// exclusive alternatives selected once per session.
// - BankServer is the system of record for balance; ATM only tracks its physical cash
// tray - hence WithdrawTransaction debits first, then dispenses, with a compensating
// credit() if dispensing fails.
class Card {
constructor(cardNumber) { this.cardNumber = cardNumber; }
}
class Account {
constructor(accountId, balance) { this.accountId = accountId; this.balance = balance; }
}
const AuthResult = Object.freeze({ SUCCESS: 'SUCCESS', INVALID_PIN: 'INVALID_PIN' });
const DebitStatus = Object.freeze({ SUCCESS: 'SUCCESS', INSUFFICIENT_FUNDS: 'INSUFFICIENT_FUNDS' });
const TransactionResult = Object.freeze({
SUCCESS: 'SUCCESS', INSUFFICIENT_FUNDS: 'INSUFFICIENT_FUNDS',
DISPENSE_FAILED: 'DISPENSE_FAILED', BAD_AMOUNT: 'BAD_AMOUNT',
});
class InsufficientCashException extends Error {}
// ---------- BankServer (interface, external authority) ----------
class BankServer {
authenticate(_card, _pin) { throw new Error('not implemented'); }
accountFor(_card) { throw new Error('not implemented'); }
debit(_account, _amount) { throw new Error('not implemented'); }
credit(_account, _amount) { throw new Error('not implemented'); }
}
class SimpleBankServer extends BankServer {
constructor() {
super();
this.pins = new Map();
this.accountsByCard = new Map();
}
enroll(card, pin, account) {
this.pins.set(card.cardNumber, pin);
this.accountsByCard.set(card.cardNumber, account);
}
authenticate(card, pin) {
return this.pins.get(card.cardNumber) === pin ? AuthResult.SUCCESS : AuthResult.INVALID_PIN;
}
accountFor(card) { return this.accountsByCard.get(card.cardNumber); }
// Atomic check-and-deduct: never leaves the account debited below zero.
debit(account, amount) {
if (account.balance < amount) return { status: DebitStatus.INSUFFICIENT_FUNDS };
account.balance -= amount;
return { status: DebitStatus.SUCCESS };
}
credit(account, amount) { account.balance += amount; }
}
// ---------- CashDispenser / DenominationHandler (Chain of Responsibility) ----------
class DenominationHandler {
constructor(value, count) {
this.value = value;
this.count = count;
this.next = null;
}
setNext(next) { this.next = next; }
// Greedily contributes as many of its own bills as possible, passes the remainder on.
handle(amount) {
const used = Math.min(Math.floor(amount / this.value), this.count);
if (used > 0) {
this.count -= used;
console.log(` dispensing ${used} x $${this.value}`);
}
const remainder = amount - used * this.value;
if (remainder > 0 && this.next != null) this.next.handle(remainder);
else if (remainder > 0) throw new InsufficientCashException(`Cannot make change for remainder $${remainder}`);
}
}
class TwentyHandler extends DenominationHandler { constructor(count) { super(20, count); } }
class FiftyHandler extends DenominationHandler { constructor(count) { super(50, count); } }
class HundredHandler extends DenominationHandler { constructor(count) { super(100, count); } }
class CashDispenser {
constructor(chainHead) { this.chainHead = chainHead; }
dispense(amount) { this.chainHead.handle(amount); }
}
// ---------- Transaction (interface, Strategy) ----------
class Transaction {
execute(_account, _bank, _atm) { throw new Error('not implemented'); }
}
class WithdrawTransaction extends Transaction {
constructor(amount) { super(); this.amount = amount; }
execute(account, bank, atm) {
if (this.amount <= 0) return TransactionResult.BAD_AMOUNT;
const result = bank.debit(account, this.amount); // atomic at the bank
if (result.status !== DebitStatus.SUCCESS) return TransactionResult.INSUFFICIENT_FUNDS;
try {
atm.dispense(this.amount);
} catch (e) {
if (!(e instanceof InsufficientCashException)) throw e;
bank.credit(account, this.amount); // compensating action - never leave a debit stranded
return TransactionResult.DISPENSE_FAILED;
}
return TransactionResult.SUCCESS;
}
}
class DepositTransaction extends Transaction {
constructor(amount) { super(); this.amount = amount; }
execute(account, bank, _atm) {
if (this.amount <= 0) return TransactionResult.BAD_AMOUNT;
bank.credit(account, this.amount);
return TransactionResult.SUCCESS;
}
}
class BalanceInquiryTransaction extends Transaction {
execute(account, _bank, _atm) {
console.log(` Balance for ${account.accountId}: $${account.balance}`);
return TransactionResult.SUCCESS;
}
}
class TransferTransaction extends Transaction {
constructor(destination, amount) { super(); this.destination = destination; this.amount = amount; }
execute(account, bank, _atm) {
const result = bank.debit(account, this.amount);
if (result.status !== DebitStatus.SUCCESS) return TransactionResult.INSUFFICIENT_FUNDS;
bank.credit(this.destination, this.amount);
return TransactionResult.SUCCESS;
}
}
// ---------- ATMState (interface, State) ----------
class ATMState {
insertCard(_card) { throw new Error('not implemented'); }
enterPin(_pin) { throw new Error('not implemented'); }
selectTransaction(_t) { throw new Error('not implemented'); }
ejectCard() { throw new Error('not implemented'); }
name() { throw new Error('not implemented'); }
}
class IdleState extends ATMState {
constructor(atm) { super(); this.atm = atm; }
insertCard(card) {
this.atm.currentCard = card;
this.atm.setState(this.atm.cardInsertedState);
}
enterPin(_pin) { console.log('Reject: insert a card first'); }
selectTransaction(_t) { console.log('Reject: insert a card first'); }
ejectCard() { console.log('Reject: no card to eject'); }
name() { return 'Idle'; }
}
class CardInsertedState extends ATMState {
constructor(atm) { super(); this.atm = atm; }
insertCard(_card) { console.log('Reject: a card is already inserted'); }
enterPin(pin) {
const result = this.atm.bank.authenticate(this.atm.currentCard, pin);
if (result === AuthResult.SUCCESS) {
this.atm.currentAccount = this.atm.bank.accountFor(this.atm.currentCard);
this.atm.setState(this.atm.authenticatedState);
} else {
console.log('Reject: invalid PIN');
this.atm.setState(this.atm.idleState);
this.atm.currentCard = null;
}
}
selectTransaction(_t) { console.log('Reject: PIN not verified yet'); }
ejectCard() { this.atm.currentCard = null; this.atm.setState(this.atm.idleState); }
name() { return 'CardInserted'; }
}
class AuthenticatedState extends ATMState {
constructor(atm) { super(); this.atm = atm; }
insertCard(_card) { console.log('Reject: session already active'); }
enterPin(_pin) { console.log('Reject: already authenticated'); }
selectTransaction(t) {
this.atm.setState(this.atm.dispensingState);
const result = t.execute(this.atm.currentAccount, this.atm.bank, this.atm);
console.log(` Transaction result: ${result}`);
this.atm.setState(this.atm.authenticatedState); // back to menu, or eject
}
ejectCard() { this.atm.currentCard = null; this.atm.currentAccount = null; this.atm.setState(this.atm.idleState); }
name() { return 'Authenticated'; }
}
// Transient state while cash is physically moving - a distinct, non-reentrant phase.
class DispensingState extends ATMState {
insertCard(_card) { console.log('Reject: dispensing in progress'); }
enterPin(_pin) { console.log('Reject: dispensing in progress'); }
selectTransaction(_t) { console.log('Reject: dispensing in progress'); }
ejectCard() { console.log('Reject: dispensing in progress'); }
name() { return 'Dispensing'; }
}
class OutOfServiceState extends ATMState {
insertCard(_card) { console.log('ATM out of service'); }
enterPin(_pin) { console.log('ATM out of service'); }
selectTransaction(_t) { console.log('ATM out of service'); }
ejectCard() { console.log('ATM out of service'); }
name() { return 'OutOfService'; }
}
// ---------- ATM (context) ----------
class ATM {
constructor(bank, cashDispenser) {
this.bank = bank;
this.cashDispenser = cashDispenser;
this.idleState = new IdleState(this);
this.cardInsertedState = new CardInsertedState(this);
this.authenticatedState = new AuthenticatedState(this);
this.dispensingState = new DispensingState();
this.outOfServiceState = new OutOfServiceState();
this.currentState = this.idleState;
this.currentCard = null;
this.currentAccount = null;
}
setState(state) { this.currentState = state; }
dispense(amount) { this.cashDispenser.dispense(amount); } // delegates to the chain
insertCard(card) { this.currentState.insertCard(card); }
enterPin(pin) { this.currentState.enterPin(pin); }
selectTransaction(t) { this.currentState.selectTransaction(t); }
ejectCard() { this.currentState.ejectCard(); }
}
// ---------- Demo ----------
function main() {
const bank = new SimpleBankServer();
const card = new Card('4111-CARD');
const account = new Account('ACC-1', 500);
const friendAccount = new Account('ACC-2', 0);
bank.enroll(card, '1234', account);
const hundreds = new HundredHandler(3);
const fifties = new FiftyHandler(2);
const twenties = new TwentyHandler(5);
hundreds.setNext(fifties);
fifties.setNext(twenties);
const dispenser = new CashDispenser(hundreds);
const atm = new ATM(bank, dispenser);
atm.insertCard(card);
console.log('State:', atm.currentState.name());
atm.enterPin('0000'); // wrong PIN -> ejects back to idle
console.log('State after bad PIN:', atm.currentState.name());
atm.insertCard(card);
atm.enterPin('1234'); // correct PIN
console.log('State:', atm.currentState.name());
atm.selectTransaction(new BalanceInquiryTransaction());
atm.selectTransaction(new WithdrawTransaction(170)); // 1x100 + 1x50 + 1x20
atm.selectTransaction(new BalanceInquiryTransaction());
atm.selectTransaction(new TransferTransaction(friendAccount, 100));
console.log(` Friend balance: $${friendAccount.balance}`);
atm.selectTransaction(new DepositTransaction(500));
atm.selectTransaction(new WithdrawTransaction(300)); // exceeds remaining cash -> compensating credit
atm.selectTransaction(new BalanceInquiryTransaction());
atm.ejectCard();
console.log('State:', atm.currentState.name());
}
if (require.main === module) main();
module.exports = {
Card, Account, AuthResult, DebitStatus, TransactionResult, BankServer, SimpleBankServer,
DenominationHandler, TwentyHandler, FiftyHandler, HundredHandler, CashDispenser,
Transaction, WithdrawTransaction, DepositTransaction, BalanceInquiryTransaction, TransferTransaction,
ATMState, IdleState, CardInsertedState, AuthenticatedState, DispensingState, OutOfServiceState, ATM,
};
// ATM Machine - full LLD implementation (single-file demo).
// No "Concurrency" badge -> single-threaded; the "must be atomic" requirement is
// modeled as BankServer.debit() being a single check-and-deduct call, not via locks.
//
// Design notes:
// - State pattern for the session: withdrawing from IdleState/CardInsertedState is
// structurally unavailable (default rejection), not a scattered runtime if.
// - Chain of Responsibility for CashDispenser: each DenominationHandler only knows its
// own denomination and passes a remainder to the next link.
// - Strategy for Transaction: withdrawal/deposit/balance/transfer are mutually
// exclusive alternatives selected once per session.
// - BankServer is the system of record for balance; ATM only tracks its physical cash
// tray - hence WithdrawTransaction debits first, then dispenses, with a compensating
// credit() if dispensing fails.
class Card {
constructor(public cardNumber: string) {}
}
class Account {
constructor(public accountId: string, public balance: number) {}
}
enum AuthResult { SUCCESS = 'SUCCESS', INVALID_PIN = 'INVALID_PIN' }
enum DebitStatus { SUCCESS = 'SUCCESS', INSUFFICIENT_FUNDS = 'INSUFFICIENT_FUNDS' }
interface DebitResult { status: DebitStatus; }
enum TransactionResult {
SUCCESS = 'SUCCESS', INSUFFICIENT_FUNDS = 'INSUFFICIENT_FUNDS',
DISPENSE_FAILED = 'DISPENSE_FAILED', BAD_AMOUNT = 'BAD_AMOUNT',
}
class InsufficientCashException extends Error {}
// ---------- BankServer (interface, external authority) ----------
interface BankServer {
authenticate(card: Card, pin: string): AuthResult;
accountFor(card: Card): Account;
debit(account: Account, amount: number): DebitResult; // atomic at the bank
credit(account: Account, amount: number): void;
}
class SimpleBankServer implements BankServer {
private pins = new Map<string, string>();
private accountsByCard = new Map<string, Account>();
enroll(card: Card, pin: string, account: Account): void {
this.pins.set(card.cardNumber, pin);
this.accountsByCard.set(card.cardNumber, account);
}
authenticate(card: Card, pin: string): AuthResult {
return this.pins.get(card.cardNumber) === pin ? AuthResult.SUCCESS : AuthResult.INVALID_PIN;
}
accountFor(card: Card): Account {
const account = this.accountsByCard.get(card.cardNumber);
if (!account) throw new Error('Unknown card');
return account;
}
// Atomic check-and-deduct: never leaves the account debited below zero.
debit(account: Account, amount: number): DebitResult {
if (account.balance < amount) return { status: DebitStatus.INSUFFICIENT_FUNDS };
account.balance -= amount;
return { status: DebitStatus.SUCCESS };
}
credit(account: Account, amount: number): void { account.balance += amount; }
}
// ---------- CashDispenser / DenominationHandler (Chain of Responsibility) ----------
abstract class DenominationHandler {
next: DenominationHandler | null = null;
constructor(public value: number, public count: number) {}
setNext(next: DenominationHandler): void { this.next = next; }
// Greedily contributes as many of its own bills as possible, passes the remainder on.
handle(amount: number): void {
const used = Math.min(Math.floor(amount / this.value), this.count);
if (used > 0) {
this.count -= used;
console.log(` dispensing ${used} x $${this.value}`);
}
const remainder = amount - used * this.value;
if (remainder > 0 && this.next != null) this.next.handle(remainder);
else if (remainder > 0) throw new InsufficientCashException(`Cannot make change for remainder $${remainder}`);
}
}
class TwentyHandler extends DenominationHandler { constructor(count: number) { super(20, count); } }
class FiftyHandler extends DenominationHandler { constructor(count: number) { super(50, count); } }
class HundredHandler extends DenominationHandler { constructor(count: number) { super(100, count); } }
class CashDispenser {
constructor(private chainHead: DenominationHandler) {}
dispense(amount: number): void { this.chainHead.handle(amount); }
}
// ---------- Transaction (interface, Strategy) ----------
interface Transaction {
execute(account: Account, bank: BankServer, atm: ATM): TransactionResult;
}
class WithdrawTransaction implements Transaction {
constructor(private amount: number) {}
execute(account: Account, bank: BankServer, atm: ATM): TransactionResult {
if (this.amount <= 0) return TransactionResult.BAD_AMOUNT;
const result = bank.debit(account, this.amount); // atomic at the bank
if (result.status !== DebitStatus.SUCCESS) return TransactionResult.INSUFFICIENT_FUNDS;
try {
atm.dispense(this.amount);
} catch (e) {
if (!(e instanceof InsufficientCashException)) throw e;
bank.credit(account, this.amount); // compensating action - never leave a debit stranded
return TransactionResult.DISPENSE_FAILED;
}
return TransactionResult.SUCCESS;
}
}
class DepositTransaction implements Transaction {
constructor(private amount: number) {}
execute(account: Account, bank: BankServer, _atm: ATM): TransactionResult {
if (this.amount <= 0) return TransactionResult.BAD_AMOUNT;
bank.credit(account, this.amount);
return TransactionResult.SUCCESS;
}
}
class BalanceInquiryTransaction implements Transaction {
execute(account: Account, _bank: BankServer, _atm: ATM): TransactionResult {
console.log(` Balance for ${account.accountId}: $${account.balance}`);
return TransactionResult.SUCCESS;
}
}
class TransferTransaction implements Transaction {
constructor(private destination: Account, private amount: number) {}
execute(account: Account, bank: BankServer, _atm: ATM): TransactionResult {
const result = bank.debit(account, this.amount);
if (result.status !== DebitStatus.SUCCESS) return TransactionResult.INSUFFICIENT_FUNDS;
bank.credit(this.destination, this.amount);
return TransactionResult.SUCCESS;
}
}
// ---------- ATMState (interface, State) ----------
interface ATMState {
insertCard(card: Card): void;
enterPin(pin: string): void;
selectTransaction(t: Transaction): void;
ejectCard(): void;
name(): string;
}
class IdleState implements ATMState {
constructor(private atm: ATM) {}
insertCard(card: Card): void {
this.atm.currentCard = card;
this.atm.setState(this.atm.cardInsertedState);
}
enterPin(_pin: string): void { console.log('Reject: insert a card first'); }
selectTransaction(_t: Transaction): void { console.log('Reject: insert a card first'); }
ejectCard(): void { console.log('Reject: no card to eject'); }
name(): string { return 'Idle'; }
}
class CardInsertedState implements ATMState {
constructor(private atm: ATM) {}
insertCard(_card: Card): void { console.log('Reject: a card is already inserted'); }
enterPin(pin: string): void {
const result = this.atm.bank.authenticate(this.atm.currentCard as Card, pin);
if (result === AuthResult.SUCCESS) {
this.atm.currentAccount = this.atm.bank.accountFor(this.atm.currentCard as Card);
this.atm.setState(this.atm.authenticatedState);
} else {
console.log('Reject: invalid PIN');
this.atm.setState(this.atm.idleState);
this.atm.currentCard = null;
}
}
selectTransaction(_t: Transaction): void { console.log('Reject: PIN not verified yet'); }
ejectCard(): void { this.atm.currentCard = null; this.atm.setState(this.atm.idleState); }
name(): string { return 'CardInserted'; }
}
class AuthenticatedState implements ATMState {
constructor(private atm: ATM) {}
insertCard(_card: Card): void { console.log('Reject: session already active'); }
enterPin(_pin: string): void { console.log('Reject: already authenticated'); }
selectTransaction(t: Transaction): void {
this.atm.setState(this.atm.dispensingState);
const result = t.execute(this.atm.currentAccount as Account, this.atm.bank, this.atm);
console.log(` Transaction result: ${result}`);
this.atm.setState(this.atm.authenticatedState); // back to menu, or eject
}
ejectCard(): void {
this.atm.currentCard = null;
this.atm.currentAccount = null;
this.atm.setState(this.atm.idleState);
}
name(): string { return 'Authenticated'; }
}
// Transient state while cash is physically moving - a distinct, non-reentrant phase.
class DispensingState implements ATMState {
insertCard(_card: Card): void { console.log('Reject: dispensing in progress'); }
enterPin(_pin: string): void { console.log('Reject: dispensing in progress'); }
selectTransaction(_t: Transaction): void { console.log('Reject: dispensing in progress'); }
ejectCard(): void { console.log('Reject: dispensing in progress'); }
name(): string { return 'Dispensing'; }
}
class OutOfServiceState implements ATMState {
insertCard(_card: Card): void { console.log('ATM out of service'); }
enterPin(_pin: string): void { console.log('ATM out of service'); }
selectTransaction(_t: Transaction): void { console.log('ATM out of service'); }
ejectCard(): void { console.log('ATM out of service'); }
name(): string { return 'OutOfService'; }
}
// ---------- ATM (context) ----------
class ATM {
currentState: ATMState;
idleState: ATMState;
cardInsertedState: ATMState;
authenticatedState: ATMState;
dispensingState: ATMState;
outOfServiceState: ATMState;
currentCard: Card | null = null;
currentAccount: Account | null = null;
constructor(public bank: BankServer, private cashDispenser: CashDispenser) {
this.idleState = new IdleState(this);
this.cardInsertedState = new CardInsertedState(this);
this.authenticatedState = new AuthenticatedState(this);
this.dispensingState = new DispensingState();
this.outOfServiceState = new OutOfServiceState();
this.currentState = this.idleState;
}
setState(state: ATMState): void { this.currentState = state; }
dispense(amount: number): void { this.cashDispenser.dispense(amount); } // delegates to the chain
insertCard(card: Card): void { this.currentState.insertCard(card); }
enterPin(pin: string): void { this.currentState.enterPin(pin); }
selectTransaction(t: Transaction): void { this.currentState.selectTransaction(t); }
ejectCard(): void { this.currentState.ejectCard(); }
}
// ---------- Demo ----------
function main(): void {
const bank = new SimpleBankServer();
const card = new Card('4111-CARD');
const account = new Account('ACC-1', 500);
const friendAccount = new Account('ACC-2', 0);
bank.enroll(card, '1234', account);
const hundreds = new HundredHandler(3);
const fifties = new FiftyHandler(2);
const twenties = new TwentyHandler(5);
hundreds.setNext(fifties);
fifties.setNext(twenties);
const dispenser = new CashDispenser(hundreds);
const atm = new ATM(bank, dispenser);
atm.insertCard(card);
console.log('State:', atm.currentState.name());
atm.enterPin('0000'); // wrong PIN -> ejects back to idle
console.log('State after bad PIN:', atm.currentState.name());
atm.insertCard(card);
atm.enterPin('1234'); // correct PIN
console.log('State:', atm.currentState.name());
atm.selectTransaction(new BalanceInquiryTransaction());
atm.selectTransaction(new WithdrawTransaction(170)); // 1x100 + 1x50 + 1x20
atm.selectTransaction(new BalanceInquiryTransaction());
atm.selectTransaction(new TransferTransaction(friendAccount, 100));
console.log(` Friend balance: $${friendAccount.balance}`);
atm.selectTransaction(new DepositTransaction(500));
atm.selectTransaction(new WithdrawTransaction(300)); // exceeds remaining cash -> compensating credit
atm.selectTransaction(new BalanceInquiryTransaction());
atm.ejectCard();
console.log('State:', atm.currentState.name());
}
main();
export {
Card, Account, AuthResult, DebitStatus, TransactionResult, BankServer, SimpleBankServer,
DenominationHandler, TwentyHandler, FiftyHandler, HundredHandler, CashDispenser,
Transaction, WithdrawTransaction, DepositTransaction, BalanceInquiryTransaction, TransferTransaction,
ATMState, IdleState, CardInsertedState, AuthenticatedState, DispensingState, OutOfServiceState, ATM,
};