LLD Hub
lldstate-patternchain-of-responsibility

How to Design an ATM Machine | LLD Interview Guide

Design an ATM system with PIN authentication, State pattern, and cash dispensing via Chain of Responsibility. Asked at banks and fintech companies.

9 April 2025·8 min read

Practice this problem

ATM Machine — get AI-scored feedback on your solution

Solve it →

The ATM Machine LLD is a great problem for practicing the State pattern and Chain of Responsibility. It's asked in fintech company interviews and tests how well you can model a system with strict state transitions and hardware constraints.

Core Entities

  • ATM — facade, delegates to subsystems
  • Card — cardNumber, PIN, linkedAccount
  • Account — balance, accountNumber
  • Transaction — type, amount, timestamp, status
  • CashDispenser — tracks denomination counts, dispenses cash
  • ATMState — interface for state behavior

State Pattern for ATM

interface ATMState {
  insertCard(atm: ATM, card: Card): void;
  enterPIN(atm: ATM, pin: string): void;
  selectTransaction(atm: ATM, type: TransactionType): void;
  withdraw(atm: ATM, amount: number): void;
  ejectCard(atm: ATM): void;
}

class IdleState implements ATMState {
  insertCard(atm: ATM, card: Card) {
    atm.setCurrentCard(card);
    atm.setState(new CardInsertedState());
  }
  enterPIN() { throw new Error("Insert card first"); }
  withdraw()  { throw new Error("Insert card first"); }
}

class CardInsertedState implements ATMState {
  enterPIN(atm: ATM, pin: string) {
    if (atm.getCurrentCard().validatePIN(pin)) {
      atm.setState(new AuthenticatedState());
    } else {
      atm.incrementPINAttempts();
      if (atm.getPINAttempts() >= 3) {
        atm.blockCard(); atm.setState(new IdleState());
      }
    }
  }
}

class AuthenticatedState implements ATMState {
  withdraw(atm: ATM, amount: number) {
    const account = atm.getLinkedAccount();
    if (account.balance < amount) throw new Error("Insufficient funds");
    atm.getCashDispenser().dispense(amount);
    account.debit(amount);
    atm.setState(new IdleState());
  }
}

Chain of Responsibility for Cash Dispensing

abstract class NoteDispenser {
  protected next: NoteDispenser | null = null;
  setNext(h: NoteDispenser) { this.next = h; return h; }
  abstract dispense(amount: number): void;
}

class FiveHundredDispenser extends NoteDispenser {
  dispense(amount: number) {
    const count = Math.floor(amount / 500);
    if (count > 0) console.log(`Dispensed ${count}×₹500`);
    this.next?.dispense(amount % 500);
  }
}
// Chain: ₹500 → ₹200 → ₹100 → ₹50

Common Questions

  • "How do you handle concurrent withdrawals?" → Account balance update must be atomic (DB transaction)
  • "What if the ATM runs out of a denomination?" → Skip that handler in the chain
  • "How do you log every transaction?" → Observer on Account, every debit/credit fires an event

Ready to practice?

Submit your solution and get AI-scored feedback on OOP, SOLID principles, design patterns, and code quality.

Solve ATM Machine