14. ATM Machine Intermediate

A. Realistic Interview Prompt

"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

D. Requirements

Functional

  1. Accept a card, prompt for and verify a PIN against the bank
  2. Support balance inquiry, cash withdrawal, deposit, and transfer
  3. Dispense withdrawn cash as an optimal mix of available denominations
  4. Eject the card at the end of a session or on cancellation
  5. Track the ATM's own cash inventory per denomination and refuse withdrawals it cannot physically fulfill

Non-Functional

E. Clarifying Questions

#QuestionWhy It MattersDesign Decision Affected
1Does 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 carefullyScope of BankServer boundary (see G, W)
2Should 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 configurableOrder of DenominationHandler chain
3Is this a single ATM, or a fleet reporting to a shared bank backend?Determines whether cross-ATM/cross-channel concurrency is explicitly in scopeScope of W
4Are deposits credited immediately or held pending (e.g. check deposits)?Affects whether deposit is a synchronous or asynchronous transactionScope of DepositTransaction

F. Assumptions

G. Actors

ActorResponsibility
CustomerInserts 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

H. Core Domain Entities

ClassResponsibilityKey AttributesKey MethodsDoes NOT Handle
ATMState (interface)One session state's legal actions—insertCard(), enterPin(), selectTransaction(), ejectCard()Anything about cash inventory or bank calls directly — delegates to the ATM context
IdleState, CardInsertedState, AuthenticatedState, DispensingState, OutOfServiceStateConcrete states———
ATM (context)Holds current state, cash inventory, delegates session actionscurrentState, cashInventorysetState(), dispense(amount)PIN/balance authority — delegated to BankServer
CashDispenserChain of Responsibility root for denomination splittingchain headdispense(amount)Whether the account has sufficient funds — that's checked before dispensing is ever invoked
DenominationHandler (abstract)One denomination's greedy contribution + pass-throughdenomination value, count in trayhandle(amount)—
Transaction (interface, Strategy)One transaction type's execution logic—execute(account, bankServer)Session state transitions
WithdrawTransaction, DepositTransaction, BalanceInquiryTransaction, TransferTransactionConcrete transactions———
BankServer (interface)External authority for auth and balance mutation—authenticate(card, pin), debit(account, amount), credit(account, amount)Physical cash

I. Class Relationships

J. Class Diagram

selects

ATM

currentState

cashInventory

+setState()

+dispense(amount)

«interface»

ATMState

+insertCard()

+enterPin()

+selectTransaction()

CashDispenser

+dispense(amount)

«abstract»

DenominationHandler

value

count

+handle(amount)

«interface»

Transaction

+execute(account, bankServer)

«interface»

BankServer

+authenticate()

+debit()

+credit()

IdleState

CardInsertedState

AuthenticatedState

WithdrawTransaction

DepositTransaction

View Mermaid source
classDiagram
  class ATM {
    currentState
    cashInventory
    +setState()
    +dispense(amount)
  }
  class ATMState {
    <<interface>>
    +insertCard()
    +enterPin()
    +selectTransaction()
  }
  class CashDispenser {
    +dispense(amount)
  }
  class DenominationHandler {
    <<abstract>>
    value
    count
    +handle(amount)
  }
  class Transaction {
    <<interface>>
    +execute(account, bankServer)
  }
  class BankServer {
    <<interface>>
    +authenticate()
    +debit()
    +credit()
  }
  ATM --> ATMState
  ATMState <|.. IdleState
  ATMState <|.. CardInsertedState
  ATMState <|.. AuthenticatedState
  ATM --> CashDispenser
  CashDispenser --> DenominationHandler
  ATM --> Transaction : selects
  Transaction <|.. WithdrawTransaction
  Transaction <|.. DepositTransaction
  WithdrawTransaction --> BankServer

K. Responsibility Mapping

ClassResponsibilityDoes NOT Handle
ATMState implementationsLegal actions for one session phaseBank calls, cash math
CashDispenser / DenominationHandler chainSplitting an amount into available denominationsWhether the withdrawal itself is authorized
Transaction implementationsOne transaction type's execution against the bankSession state transitions
BankServerAuthentication, balance authorityPhysical cash
ATM (context)Session orchestration, cash inventoryAccount truth

L. SOLID Analysis

PrincipleHow It's AppliedNaive Violation
SRPState classes own session legality; dispenser chain owns cash math; transactions own bank interactionOne ATM god-class with a mode string field and every operation checking it inline
OCPA new denomination is a new chain link; a new transaction type is a new Transaction implementationA single dispense() method with a hardcoded denomination list that must be edited for a new note value
LSPEvery ATMState honestly rejects actions illegal for that phase rather than silently no-oppingOutOfServiceState.selectTransaction() quietly returning a default success instead of a clear rejection
ISPTransaction and BankServer both stay small, focused interfacesA single "AccountService" interface mixing authentication, balance, and physical-cash concerns
DIPATM depends on the BankServer interface, not a concrete bank integrationHardcoding a specific bank's API calls directly inside WithdrawTransaction

M. Design Patterns

PatternProblem It SolvesWhere It AppearsWhen NOT to Use
StateSession legality genuinely depends on phase, and illegal actions are a real correctness/safety riskATM / ATMState hierarchySkip only for a trivial single-operation kiosk with no session concept
Chain of ResponsibilityDenomination splitting is a sequence of independent handlers, each contributing what it can and passing the remainder onCashDispenser / DenominationHandlerSkip if the ATM only ever dispenses one fixed denomination
StrategyTransaction type is a mutually exclusive choice made once per sessionTransaction hierarchySkip 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?

BehaviorCurrent DesignAnticipated ChangeProtecting Abstraction
Denomination setFixed set of notesNew note denominations, coin dispensingNew DenominationHandler link, zero change to CashDispenser
Transaction typesWithdraw/deposit/balance/transferBill pay, mobile top-upNew Transaction implementation
Auth methodCard + PINCard + biometric, cardless (phone) authBankServer.authenticate() signature generalized to an auth-context object rather than being tied to a physical card

O. Design Evolution

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);
}

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

R. End-to-End Use Case

insertCard(card) → CardInsertedState
enterPin("1234") → bank.authenticate() succeeds → AuthenticatedState
selectTransaction(Withdraw, 270)
  │  WithdrawTransaction.execute():
  │    bank.debit(account, 270) → success (atomic at bank)
  │    atm.dispense(270):
  │      HundredHandler: 2×$100, remainder 70
  │      FiftyHandler: 1×$50, remainder 20
  │      TwentyHandler: 1×$20, remainder 0 — chain stops
  ▼
ejectCard() → IdleState

S. Data Structures

StructureUsed ForWhyAlternative & Trade-off
Linked chain of DenominationHandler, ordered largest-to-smallestGreedy denomination splittingMinimizes note count for the common case; each handler only needs to know its own value and successorA 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 inventoryTracking remaining notes per denominationO(1) availability check per handler—

T. Complexity Analysis

OperationTimeSpace
Dispensing (chain traversal)O(number of denominations) — a small constantO(1)
Authentication / debitO(1) local, dominated by the network round-trip to BankServerO(1)

U. Testing Strategy

V. Interviewer Follow-Ups

Follow-UpTestingDesign ChangeClasses ChangedPattern/Trade-off
"What if the ATM runs out of a denomination mid-dispense?"Chain-of-Responsibility failure handlingThe failing handler throws; already-dispensed notes must be tracked for reconciliation, and the transaction compensates via bank.credit()DenominationHandler, WithdrawTransactionDirectly exercises Q/Z
"Two ATMs, same account, same instant, both requesting the full balance."The check-then-act balance race, once moreNone 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 WThe account-balance instance of this document's most repeated concurrency lesson
"Support cardless, phone-based authentication."Whether auth was tied unnecessarily to a physical cardGeneralize authenticate()'s input to an auth-context abstractionBankServer interface, ATMState entry pointTests 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).

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

LayerWhat Belongs Here
LLDSession state machine, denomination dispensing chain, transaction Strategy, debit/dispense ordering
HLDBank's own distributed ledger, fraud detection, ATM fleet monitoring/cash replenishment logistics
Explicitly out of scopeThe bank's internal balance-storage architecture — the ATM only needs BankServer to guarantee atomicity, not to design that system itself

Z. Failure Scenarios

AA. Common Bad Designs

AB. Design Trade-Offs

DecisionOption AOption BRecommendation
Dispensing algorithmGreedy largest-denomination-first (Chain of Responsibility)DP-based minimum-notes solverGreedy — real ATM denomination sets are chosen specifically to make greedy optimal, and it's dramatically simpler to implement and reason about
Debit/dispense orderingDebit first, then dispense, with compensating credit on dispense failureDispense first, then debitDebit 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.

AE. Online Solution Comparison

SourceApproachStrengthWeaknessWorth Learning
AlgoMaster.io — Design ATMState-pattern-based ATM session model, with each state handling its own set of legal operations directlyConfirms State as the headline pattern for the session; clean per-state operation ownershipPublic outline does not foreground the cross-channel balance race or the debit-before-dispense compensating-action orderingValidates the State-machine structure; this document's addition is the concurrency and failure-recovery depth
AlgoMaster.io — State Design PatternGeneral State pattern reference used as the theoretical basis for the ATM's session designClear, general explanation of when State beats a flags-and-conditionals approachNot ATM-specific — a pattern reference, not a worked exampleUseful as the pattern-theory cross-check underlying the ATM's headline choice

AF. Interview Cheat Sheet

ProblemATM Machine
DifficultyIntermediate
Core requirementsCard/PIN auth, balance inquiry, withdrawal with denomination dispensing, deposit
Key clarificationsATM vs. bank as source of truth? Dispensing optimization goal? Single machine or fleet? Deposit crediting timing?
Core entitiesATM, ATMState, CashDispenser, DenominationHandler, Transaction, BankServer
Key relationshipsState drives session legality; Chain of Responsibility splits denominations; Strategy selects transaction type
Key patternsState (headline), Chain of Responsibility (dispensing), Strategy (transaction type)
Most important decisionDebit-then-dispense ordering with a compensating credit on dispense failure
Likely-changing behaviorDenomination set, transaction types, auth method
Concurrency concernCross-channel check-then-act balance race, resolved by atomic debit at the bank, not at the ATM
Top follow-upsMid-dispense denomination shortage, concurrent withdrawal race, cardless authentication
Common mistakesMode-flag ATM class; hand-rolled dispensing loop; dispense-before-debit ordering; ATM treated as balance authority
What NOT to over-engineerDon'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());
    }
}