Compile Ready
All low level design problems
Low Level Design/Beginner/Core Systems

Design an ATM

A textbook State-pattern problem — card, PIN, selection, and dispense states with a cash-management backend.

Beginner 45m interview 14m read High frequency Popularity 90
State Chain of Responsibility Singleton Amazon PayPal Oracle Microsoft

Problem Statement

Design an ATM that accepts one card at a time, authenticates the card holder with a PIN, allows balance inquiry and cash withdrawal, and returns the card safely at the end of the session.

The main design challenge is to keep the session lifecycle explicit. The user moves conceptually through Idle, CardInserted, Authenticated, SelectingTransaction, and Dispensing. The reference implementation names CardInserted as HasCardState and represents transaction selection plus execution with AuthenticatedState and TransactionState.

Business context

ATM is a classic low-level design interview problem because it combines UI flow, account safety, and physical hardware constraints in a compact model.

Interviewers expect a candidate to avoid a giant condition-heavy controller. A strong design makes each state responsible for the actions allowed at that point, delegates PIN and balance operations to the bank boundary, and treats the cash cassette as one shared hardware resource that dispenses notes through a highest-denomination-first chain.

Functional Requirements

  • Accept a card only when the ATM is idle and bind it to the current session.

  • Authenticate the card holder by validating the entered PIN through a bank or account service.

  • Maintain the active card and account only for the lifetime of the session.

  • Allow an authenticated user to check account balance through the bank service.

  • Allow an authenticated user to select a transaction such as withdrawal or deposit.

  • Withdraw cash only when the amount is positive, the account has enough balance, and the dispenser can satisfy the amount with available notes.

  • Dispense cash using a highest-denomination-first note chain such as 2000, then 500, then 100.

  • Allow card ejection from any active session and return the ATM to idle with session data cleared.

  • Reject invalid actions in the current state, such as withdrawing before PIN validation.

Non-Functional Requirements

Session correctness

Only one card session is active in the base design, and every eject or failed authentication path clears the active card and account.

Financial consistency

A withdrawal must not debit the account unless the dispenser can serve the amount, and it must not dispense unless the debit succeeds.

Cash inventory integrity

The dispenser owns note counts and updates them atomically per denomination so software balance never drifts from cassette balance.

Extensibility

New states, transaction types, and denominations should be additive through new state handlers or chain links, not edits across the controller.

Security

PIN validation belongs behind the bank boundary; production systems hash PIN data, limit attempts, and audit every account mutation.

Low latency

State transitions and denomination checks are in-memory and bounded by a tiny number of states and note handlers.

Requirement Clarification

QCan the ATM serve more than one card at the same time?

No. Assume one physical terminal with one active session. Multiple ATMs would each have their own session context and shared bank backend.

QWhich denominations should the cash dispenser support?

Use 2000, 500, and 100 in the base design. The chain is ordered from highest to lowest denomination so larger notes are consumed first.

QHow should balance inquiry work?

It is read-only after authentication. The reference bank service exposes getBalance, so the ATM UI can display the balance without changing session state.

QWhat happens after an invalid PIN?

The base implementation prints an invalid PIN message, clears the session, and returns to idle. A production version would count attempts and may retain or block the card.

QShould the account be debited before checking cash availability?

No. First validate that the dispenser can serve the amount, then debit the account, then dispense notes. This avoids charging for impossible cash.

QIs the cash dispenser a Singleton?

Conceptually yes for one terminal because there is only one physical cassette inventory. The reference code injects one CashDispenser instance, which keeps the singleton resource testable.

UML Class Diagram

Rendering diagram…
The diagram uses the real reference names: **HasCardState** is the CardInserted phase, **AuthenticatedState** lets the user choose a transaction, and **TransactionState** executes withdrawal or deposit before returning to authenticated.

Sequence Diagram

Rendering diagram…
The withdrawal path checks cash availability before account debit, then lets the denomination chain release notes. Balance inquiry is read-only and does not change state.

Entity Identification

ATM

Session context and public façade. It delegates user actions to the active state, stores current card/account, and coordinates bank and dispenser calls.

statebankServicecashDispensercurrentCardcurrentAccount

ATMState

State interface for all user actions. Default methods reject invalid operations, so each concrete state only implements what it allows.

insertCardenterPinselectOperationwithdrawdepositejectCard

IdleState

Represents the terminal before any card is present. It accepts a card, stores it on the ATM, and moves the session to HasCardState.

insertCardejectCardname

HasCardState

CardInserted phase. It validates the PIN through BankService, loads the account on success, or clears the session on failure.

enterPinname

AuthenticatedState

Authenticated phase. It lets the user choose a transaction and supports card ejection without losing account correctness.

selectOperationejectCardname

TransactionState

Selected transaction phase. It stores the selected operation, executes withdrawal or deposit, and returns to authenticated for the next action.

operationwithdrawdepositselectOperation

Card

Immutable card data used to identify the bank account for the current session.

cardNumberaccountId

Account

Bank account aggregate with synchronized PIN check, balance read, debit, and credit operations.

idpinbalance

BankService

Boundary to account data. It registers accounts, validates PINs, exposes balance inquiry, and performs debit or credit.

accountsvalidatePingetBalancedebitcredit

CashDispenser

Single cash hardware façade for the terminal. It owns the head of the denomination chain and validates amounts before dispensing.

headcanDispensedispense

DispenseChain

Chain interface implemented by note handlers. Each link can validate or dispense part of the requested amount and delegate the remainder.

setNextcanDispensedispense

NoteDispenser

One denomination handler. It uses as many of its notes as possible, updates its count on dispense, and passes the remainder to the next link.

denominationnoteCountnext

Design Patterns Used

State

The session lifecycle is represented by ATMState implementations. Invalid actions are rejected by the current state instead of a long conditional block inside ATM.

Chain of Responsibility

CashDispenser delegates to a linked chain of NoteDispenser handlers. Each handler consumes its denomination and passes the remaining amount downward.

Singleton

A physical ATM has one cash cassette inventory. The reference design injects one CashDispenser instance so tests stay clean, while production can expose that same resource through a singleton provider per terminal.

Step-by-Step Design

  1. 1Define state-driven user actions

    Start with ATMState as the contract for card insertion, PIN entry, transaction selection, withdrawal, deposit, and eject. Default methods reject actions that the current state does not allow.

    public interface ATMState {
        default void withdraw(ATM atm, int amount) {
            throw new IllegalStateException("Cannot withdraw now");
        }
    
        String name();
    }
  2. 2Map lifecycle phases to concrete states

    Use IdleState for no card, HasCardState for CardInserted, AuthenticatedState for selecting the next transaction, and TransactionState for executing the selected operation.

  3. 3Keep ATM as a delegating context

    Public methods on ATM are thin. They call the same method on the current state, while package-level helpers such as performWithdrawal perform the coordinated bank and dispenser work.

  4. 4Authenticate and read balances through the bank boundary

    BankService owns account lookup, PIN validation, balance inquiry, debit, and credit. The ATM does not inspect or mutate account fields directly.

    public int getBalance(String accountId) {
        return getAccount(accountId).getBalance();
    }
    
    public void debit(String accountId, int amount) {
        getAccount(accountId).debit(amount);
    }
  5. 5Build a highest-denomination-first dispense chain

    Create one NoteDispenser per denomination and link 2000 to 500 to 100. This keeps denomination logic local to each handler.

    NoteDispenser twoThousand = new NoteDispenser(2000, notesOf2000);
    NoteDispenser fiveHundred = new NoteDispenser(500, notesOf500);
    NoteDispenser oneHundred = new NoteDispenser(100, notesOf100);
    twoThousand.setNext(fiveHundred);
    fiveHundred.setNext(oneHundred);
  6. 6Withdraw in a failure-safe order

    Validate amount, verify dispenser capability, debit the bank account, and only then dispense notes. If either validation or debit fails, no cash leaves the ATM.

  7. 7Eject card as a universal reset path

    The default state behavior can clear the current session and return to idle. Active states may override it to print messages, but they preserve the same invariant.

Complete Java Implementation

Loading…

Explanation of Every Class

ATMState

Interface for state-specific behavior. Its default methods throw for invalid actions, and the default eject path clears the session and returns the ATM to IdleState.

IdleState

The only state that accepts a new card. It validates the card object, stores it on ATM, transitions to HasCardState, and treats eject as a harmless no-op.

HasCardState

Handles PIN entry after card insertion. It asks BankService to validate the PIN, loads the account on success, and clears the session back to idle on failure.

AuthenticatedState

Represents a verified user who can choose a transaction or eject the card. It creates TransactionState with the selected operation string.

TransactionState

Stores the selected operation in normalized form. It executes withdrawal or deposit only when the operation matches, then returns the ATM to AuthenticatedState.

ATM

Context object and public façade. It delegates user actions to the current state, holds the active card/account, and coordinates withdrawal as dispenser check, bank debit, then cash dispense.

Card

Immutable value object with a card number and account id. It is intentionally small because the bank service owns account validation and balance data.

Account

Synchronized account model with id, PIN, and balance. It validates PIN, returns balance, and guards debit against insufficient funds.

BankService

In-memory bank boundary backed by an account map. It registers accounts, resolves accounts by id, validates PIN, and exposes debit, credit, and balance inquiry.

DispenseChain

Interface for denomination handlers. It defines linking, capability check, and dispense operations so the cash path can be extended one handler at a time.

NoteDispenser

Concrete chain link for one denomination. It calculates usable notes, delegates the remaining amount, and decrements its own note count during dispense.

CashDispenser

Cash hardware façade. It builds the 2000 to 500 to 100 chain, rejects non-positive or non-100-multiple amounts, and delegates validation and dispensing to the chain head.

Dry Run

Sample input

Account A1 has PIN 1234 and balance 10000. The dispenser has one 2000 note, three 500 notes, and ten 100 notes. Actions: insert card C1, enter PIN, check balance, withdraw 2600, eject card.

StepUser actionState beforeMain collaboratorState afterResult
1insertCard(C1)IDLEIdleStateHAS_CARDCurrent card set to C1
2enterPin(1234)HAS_CARDBankService.validatePinAUTHENTICATEDAccount A1 loaded
3balance inquiryAUTHENTICATEDBankService.getBalanceAUTHENTICATEDBalance shown as 10000
4selectOperation(WITHDRAW)AUTHENTICATEDAuthenticatedStateTRANSACTION_WITHDRAWOperation stored
5withdraw(2600)TRANSACTION_WITHDRAWCashDispenser and BankServiceAUTHENTICATEDDebit 2600, dispense 1x2000 plus 1x500 plus 1x100
6ejectCard()AUTHENTICATEDAuthenticatedStateIDLESession cleared

Step 5 demonstrates the critical ordering: cash capability is checked first, the account is debited second, and the chain releases notes third. The user stays authenticated after a completed transaction and may choose another operation or eject.

Complexity Analysis

OperationTimeSpaceNote
insert card / eject cardO(1)O(1)Only state and session references are updated.
PIN authenticationO(1) averageO(1)Hash map lookup by account id plus a synchronized PIN comparison in the reference model.
balance inquiryO(1) averageO(1)BankService resolves the account and reads the synchronized balance.
withdrawO(D)O(1)D is the number of denomination handlers. The reference chain has D = 3.
depositO(1) averageO(1)A bank lookup followed by a synchronized credit.

The ATM state machine is constant sized. Withdrawal is linear in denomination count, which is effectively constant for normal ATM cassettes. If denominations become dynamic or non-canonical, consider a planner that finds an optimal note combination before mutating inventory.

Extensibility

New denominations

Add another NoteDispenser link and place it in the chain order. No ATM state or bank code changes.

New transaction type

Add an operation branch in TransactionState or introduce transaction command objects if the list grows beyond a few operations.

Multiple accounts per card

Let Card carry account choices and add an account-selection state after authentication before transaction selection.

External bank integration

Replace the in-memory BankService map with a gateway or repository while keeping the ATM state model unchanged.

Receipts and audit

Emit a transaction record after each debit, credit, balance inquiry, and eject event without changing the state transition contracts.

Alternative Designs

Switch-based ATM controller

Keep an enum for the current state and use conditionals inside every ATM method to decide what is allowed.

Tradeoffs

Simpler for a tiny demo, but each new state or action edits the central class and makes invalid transitions easy to miss.

Command objects for transactions

Represent withdrawal, deposit, balance inquiry, and transfer as command classes selected after authentication.

Tradeoffs

Cleaner when transaction types grow, but extra abstraction can feel heavy for the beginner version with only a few operations.

Hard Singleton cash dispenser

Expose CashDispenser.getInstance and let every ATM call the global instance directly.

Tradeoffs

Protects one hardware inventory, but hurts tests and multi-terminal simulations. Dependency injection of one shared instance is usually the better Singleton variant.

Optimal note planner

Instead of greedy highest-first dispensing, compute a note combination that satisfies the amount while preserving scarce denominations.

Tradeoffs

Useful for unusual denominations or cash optimization, but overkill for standard ATM notes and makes the design less interview-friendly.

Common Mistakes

  • ×

    Putting every action check inside ATM instead of letting the current state own valid and invalid behavior.

  • ×

    Debiting the account before checking whether the cash dispenser can serve the requested amount.

  • ×

    Dispensing cash before confirming the bank debit succeeded.

  • ×

    Forgetting to clear current card and account on eject or failed authentication.

  • ×

    Letting every denomination know about all other denominations instead of using a linked chain.

  • ×

    Treating the cash dispenser as many independent objects, which creates conflicting note counts for one physical cassette.

  • ×

    Making balance inquiry mutate session or account state even though it is read-only.

  • ×

    Storing raw PINs in a production design rather than using hashing, encryption, retry limits, and audit controls.

Follow-up Interview Questions

QHow would you add balance inquiry as a first-class ATM action?

Add balanceInquiry to ATMState with a default rejection, implement it in AuthenticatedState or TransactionState, and delegate to BankService.getBalance for the active account.

QWhat if the dispenser check passes but bank debit fails?

No cash should be dispensed. The reference order already checks cash first, then calls bank debit, then dispenses only after debit returns successfully.

QWhat if the dispenser jams after the account was debited?

Production needs hardware acknowledgements, reversal or adjustment transactions, and reconciliation logs. The interview model can mention this as a reliability extension.

QHow would you enforce one cash dispenser instance per terminal?

Use a terminal-scoped singleton provider or dependency injection container that returns the same CashDispenser for a given ATM id, and keep its methods synchronized.

QHow do you support multiple accounts on one card?

After PIN validation, introduce an account-selection state, store the chosen account on ATM, and keep transaction logic unchanged.

QWhy is State better than an enum and switch here?

Each state owns its legal actions and transitions. Adding a state becomes adding a class, not editing every public ATM method.

Production Considerations

PIN and card security

Never store raw PINs. Use secure PIN verification, attempt limits, card blocking rules, encrypted transport, and careful logging that excludes secrets.

Transaction atomicity

Coordinate bank debit, cash dispense acknowledgement, and reversal paths so outages or hardware faults do not leave customers charged incorrectly.

Cash reconciliation

Track cassette inventory, physical refill events, rejected notes, and daily balancing against the bank ledger.

Remote bank failures

Timeouts, retries, offline mode, and idempotency keys are required when the ATM calls a remote banking network.

Observability and audit

Emit metrics and audit events for authentication failures, declined withdrawals, dispenser errors, balance inquiries, and cash-low thresholds.

What Interviewers Look For

  • Can the candidate explain the lifecycle as states instead of booleans such as cardPresent and authenticated?

  • Do they keep account operations behind BankService and avoid direct balance mutation from UI code?

  • Do they protect the withdrawal ordering so users are not debited without cash?

  • Do they model cash dispensing with a clean denomination chain rather than hard-coded nested conditionals?

  • Do they recognize CashDispenser as one shared hardware resource and discuss Singleton without sacrificing testability?

  • Can they state how balance inquiry fits without mutating the session?

Quiz

0/6 answered

  1. 1.Which component should reject a withdrawal before a valid PIN is entered?

  2. 2.Why should the ATM call canDispense before debiting the account?

  3. 3.What does a NoteDispenser do in the chain?

  4. 4.What is the best interpretation of Singleton for CashDispenser in this design?

  5. 5.Which operation should be read-only after authentication?

  6. 6.Why does TransactionState return to AuthenticatedState after a withdrawal or deposit?

Practice Variants

Make balance inquiry a full state action

Beginner

Add a balance inquiry method to the state interface, implement it for authenticated users, and return a display-friendly result without changing balance.

Add daily withdrawal limits

Intermediate

Track per-account or per-card withdrawal totals and reject requests beyond the configured limit before debiting.

Support multiple accounts per card

Intermediate

After PIN validation, add an account-selection phase and let withdrawal, deposit, and balance inquiry operate on the selected account.

Handle dispenser failure and reversal

Advanced

Model hardware acknowledgement and add a reversal path when debit succeeds but cash dispense fails.

Flashcards

Cheat Sheet

Core model: ATM is the context. ATMState is the lifecycle interface. IdleState, HasCardState, AuthenticatedState, and TransactionState own allowed actions.

Lifecycle: Idle accepts a card. HasCard validates PIN. Authenticated selects a transaction or ejects. TransactionState executes withdrawal or deposit and returns to Authenticated.

Bank boundary: BankService owns account lookup, PIN validation, debit, credit, and balance inquiry. Account owns synchronized balance mutation.

Cash path: CashDispenser owns a highest-first chain: 2000 to 500 to 100. NoteDispenser handles one denomination and delegates the remainder.

Patterns: State for session flow, Chain of Responsibility for denominations, Singleton-style single CashDispenser resource per terminal.

Withdrawal invariant: check dispenser capability before debit; dispense only after debit succeeds; clear session on eject or failed authentication.

Complexity: state transitions O(1), balance inquiry O(1) average, withdrawal O(D) for D denomination handlers.

References