Skip to content
Unverified — AI-generated content. Help verify this page

Design ATM Machine ​

The ATM machine is a classic LLD interview problem that tests your ability to model a state machine cleanly. The ATM has a well-defined lifecycle: idle, card inserted, PIN validated, transaction selected, processing, dispensing, and back to idle. Each state has specific allowed transitions, and invalid transitions must be rejected gracefully.

Requirements ​

Functional Requirements ​

#RequirementDetails
FR-1Card insertionAccept and validate bank cards
FR-2PIN authenticationValidate PIN with max 3 attempts
FR-3Balance inquiryDisplay current account balance
FR-4Cash withdrawalDispense cash if sufficient balance and ATM has funds
FR-5Cash depositAccept and credit cash deposits
FR-6Fund transferTransfer between accounts at the same bank
FR-7Receipt printingPrint transaction receipt
FR-8Card ejectionReturn card on completion or timeout

Non-Functional Requirements ​

  • Single ATM machine (not distributed)
  • Thread-safe transaction processing
  • Timeout handling (eject card after inactivity)
  • Denomination-aware dispensing

State Machine Design ​

The state machine is the backbone of this design. Every user interaction triggers a state transition, and the ATM only accepts inputs valid for its current state.

Core Entities ​

Implementation ​

Enums and Types ​

TypeScript:

typescript
enum TransactionType {
  BALANCE_INQUIRY = "BALANCE_INQUIRY",
  WITHDRAWAL = "WITHDRAWAL",
  DEPOSIT = "DEPOSIT",
  TRANSFER = "TRANSFER",
}

enum Denomination {
  HUNDRED = 100,
  FIVE_HUNDRED = 500,
  THOUSAND = 1000,
  TWO_THOUSAND = 2000,
}

interface Card {
  cardNumber: string;
  bankCode: string;
  expiryDate: Date;
}

interface TransactionResult {
  success: boolean;
  message: string;
  balance?: number;
  dispensedCash?: Map<Denomination, number>;
}

Python:

python
from enum import Enum
from dataclasses import dataclass, field
from datetime import datetime
from abc import ABC, abstractmethod

class TransactionType(Enum):
    BALANCE_INQUIRY = "BALANCE_INQUIRY"
    WITHDRAWAL = "WITHDRAWAL"
    DEPOSIT = "DEPOSIT"
    TRANSFER = "TRANSFER"

class Denomination(Enum):
    HUNDRED = 100
    FIVE_HUNDRED = 500
    THOUSAND = 1000
    TWO_THOUSAND = 2000

@dataclass
class Card:
    card_number: str
    bank_code: str
    expiry_date: datetime

@dataclass
class TransactionResult:
    success: bool
    message: str
    balance: float | None = None
    dispensed_cash: dict[Denomination, int] | None = None

Bank Service Interface ​

TypeScript:

typescript
interface BankService {
  validateCard(cardNumber: string): boolean;
  validatePin(cardNumber: string, pin: string): boolean;
  getBalance(accountId: string): number;
  withdraw(accountId: string, amount: number): boolean;
  deposit(accountId: string, amount: number): boolean;
  transfer(fromAccount: string, toAccount: string, amount: number): boolean;
  getAccountId(cardNumber: string): string;
}

class MockBankService implements BankService {
  private accounts: Map<string, { pin: string; balance: number; accountId: string }>;

  constructor() {
    this.accounts = new Map([
      ["4111111111111111", { pin: "1234", balance: 50000, accountId: "ACC001" }],
      ["4222222222222222", { pin: "5678", balance: 30000, accountId: "ACC002" }],
    ]);
  }

  validateCard(cardNumber: string): boolean {
    return this.accounts.has(cardNumber);
  }

  validatePin(cardNumber: string, pin: string): boolean {
    const account = this.accounts.get(cardNumber);
    return account?.pin === pin;
  }

  getBalance(accountId: string): number {
    for (const [, acc] of this.accounts) {
      if (acc.accountId === accountId) return acc.balance;
    }
    return 0;
  }

  withdraw(accountId: string, amount: number): boolean {
    for (const [, acc] of this.accounts) {
      if (acc.accountId === accountId && acc.balance >= amount) {
        acc.balance -= amount;
        return true;
      }
    }
    return false;
  }

  deposit(accountId: string, amount: number): boolean {
    for (const [, acc] of this.accounts) {
      if (acc.accountId === accountId) {
        acc.balance += amount;
        return true;
      }
    }
    return false;
  }

  transfer(fromAccount: string, toAccount: string, amount: number): boolean {
    if (this.withdraw(fromAccount, amount)) {
      if (this.deposit(toAccount, amount)) return true;
      this.deposit(fromAccount, amount); // rollback
    }
    return false;
  }

  getAccountId(cardNumber: string): string {
    return this.accounts.get(cardNumber)?.accountId ?? "";
  }
}

Python:

python
class BankService(ABC):
    @abstractmethod
    def validate_card(self, card_number: str) -> bool: ...

    @abstractmethod
    def validate_pin(self, card_number: str, pin: str) -> bool: ...

    @abstractmethod
    def get_balance(self, account_id: str) -> float: ...

    @abstractmethod
    def withdraw(self, account_id: str, amount: float) -> bool: ...

    @abstractmethod
    def deposit(self, account_id: str, amount: float) -> bool: ...

    @abstractmethod
    def transfer(self, from_acc: str, to_acc: str, amount: float) -> bool: ...

    @abstractmethod
    def get_account_id(self, card_number: str) -> str: ...

class MockBankService(BankService):
    def __init__(self):
        self.accounts = {
            "4111111111111111": {"pin": "1234", "balance": 50000, "account_id": "ACC001"},
            "4222222222222222": {"pin": "5678", "balance": 30000, "account_id": "ACC002"},
        }

    def validate_card(self, card_number: str) -> bool:
        return card_number in self.accounts

    def validate_pin(self, card_number: str, pin: str) -> bool:
        acc = self.accounts.get(card_number)
        return acc is not None and acc["pin"] == pin

    def get_balance(self, account_id: str) -> float:
        for acc in self.accounts.values():
            if acc["account_id"] == account_id:
                return acc["balance"]
        return 0

    def withdraw(self, account_id: str, amount: float) -> bool:
        for acc in self.accounts.values():
            if acc["account_id"] == account_id and acc["balance"] >= amount:
                acc["balance"] -= amount
                return True
        return False

    def deposit(self, account_id: str, amount: float) -> bool:
        for acc in self.accounts.values():
            if acc["account_id"] == account_id:
                acc["balance"] += amount
                return True
        return False

    def transfer(self, from_acc: str, to_acc: str, amount: float) -> bool:
        if self.withdraw(from_acc, amount):
            if self.deposit(to_acc, amount):
                return True
            self.deposit(from_acc, amount)  # rollback
        return False

    def get_account_id(self, card_number: str) -> str:
        acc = self.accounts.get(card_number)
        return acc["account_id"] if acc else ""

Cash Dispenser ​

TypeScript:

typescript
class CashDispenser {
  private cash: Map<Denomination, number> = new Map();

  constructor() {
    // Initialize with default cash
    this.cash.set(Denomination.TWO_THOUSAND, 100);
    this.cash.set(Denomination.THOUSAND, 200);
    this.cash.set(Denomination.FIVE_HUNDRED, 300);
    this.cash.set(Denomination.HUNDRED, 500);
  }

  canDispense(amount: number): boolean {
    let remaining = amount;
    const denominations = [
      Denomination.TWO_THOUSAND,
      Denomination.THOUSAND,
      Denomination.FIVE_HUNDRED,
      Denomination.HUNDRED,
    ];

    const tempCash = new Map(this.cash);

    for (const denom of denominations) {
      const available = tempCash.get(denom) ?? 0;
      const needed = Math.floor(remaining / denom);
      const used = Math.min(needed, available);
      remaining -= used * denom;
    }

    return remaining === 0;
  }

  dispense(amount: number): Map<Denomination, number> {
    const result = new Map<Denomination, number>();
    let remaining = amount;

    const denominations = [
      Denomination.TWO_THOUSAND,
      Denomination.THOUSAND,
      Denomination.FIVE_HUNDRED,
      Denomination.HUNDRED,
    ];

    for (const denom of denominations) {
      const available = this.cash.get(denom) ?? 0;
      const needed = Math.floor(remaining / denom);
      const used = Math.min(needed, available);

      if (used > 0) {
        result.set(denom, used);
        this.cash.set(denom, available - used);
        remaining -= used * denom;
      }
    }

    return result;
  }

  totalCash(): number {
    let total = 0;
    for (const [denom, count] of this.cash) {
      total += denom * count;
    }
    return total;
  }
}

Python:

python
class CashDispenser:
    def __init__(self):
        self.cash: dict[Denomination, int] = {
            Denomination.TWO_THOUSAND: 100,
            Denomination.THOUSAND: 200,
            Denomination.FIVE_HUNDRED: 300,
            Denomination.HUNDRED: 500,
        }

    def can_dispense(self, amount: int) -> bool:
        remaining = amount
        for denom in sorted(self.cash.keys(), key=lambda d: d.value, reverse=True):
            available = self.cash[denom]
            needed = remaining // denom.value
            used = min(needed, available)
            remaining -= used * denom.value
        return remaining == 0

    def dispense(self, amount: int) -> dict[Denomination, int]:
        result: dict[Denomination, int] = {}
        remaining = amount

        for denom in sorted(self.cash.keys(), key=lambda d: d.value, reverse=True):
            available = self.cash[denom]
            needed = remaining // denom.value
            used = min(needed, available)

            if used > 0:
                result[denom] = used
                self.cash[denom] -= used
                remaining -= used * denom.value

        return result

    def total_cash(self) -> int:
        return sum(d.value * c for d, c in self.cash.items())

State Pattern Implementation ​

TypeScript:

typescript
interface ATMState {
  insertCard(atm: ATM, card: Card): void;
  enterPin(atm: ATM, pin: string): void;
  selectTransaction(atm: ATM, type: TransactionType): void;
  confirmTransaction(atm: ATM, amount: number, targetAccount?: string): TransactionResult;
  cancel(atm: ATM): void;
}

class IdleState implements ATMState {
  insertCard(atm: ATM, card: Card): void {
    if (atm.bankService.validateCard(card.cardNumber)) {
      atm.currentCard = card;
      atm.pinAttempts = 0;
      atm.setState(new CardInsertedState());
      console.log("Card accepted. Please enter your PIN.");
    } else {
      console.log("Invalid card. Please try another card.");
    }
  }

  enterPin(): void { console.log("Please insert a card first."); }
  selectTransaction(): void { console.log("Please insert a card first."); }
  confirmTransaction(): TransactionResult {
    return { success: false, message: "Please insert a card first." };
  }
  cancel(): void { console.log("No active session."); }
}

class CardInsertedState implements ATMState {
  insertCard(): void { console.log("Card already inserted."); }

  enterPin(atm: ATM, pin: string): void {
    if (atm.bankService.validatePin(atm.currentCard!.cardNumber, pin)) {
      atm.accountId = atm.bankService.getAccountId(atm.currentCard!.cardNumber);
      atm.setState(new PinEnteredState());
      console.log("PIN verified. Select a transaction.");
    } else {
      atm.pinAttempts++;
      if (atm.pinAttempts >= 3) {
        console.log("Too many failed attempts. Card retained.");
        atm.resetSession();
        atm.setState(new IdleState());
      } else {
        console.log(`Wrong PIN. ${3 - atm.pinAttempts} attempts remaining.`);
      }
    }
  }

  selectTransaction(): void { console.log("Please enter your PIN first."); }
  confirmTransaction(): TransactionResult {
    return { success: false, message: "Please enter your PIN first." };
  }
  cancel(atm: ATM): void {
    console.log("Session cancelled. Ejecting card.");
    atm.resetSession();
    atm.setState(new IdleState());
  }
}

class PinEnteredState implements ATMState {
  insertCard(): void { console.log("Card already inserted."); }
  enterPin(): void { console.log("PIN already verified."); }

  selectTransaction(atm: ATM, type: TransactionType): void {
    atm.currentTransaction = type;
    atm.setState(new TransactionSelectedState());
    console.log(`Transaction selected: ${type}`);
  }

  confirmTransaction(): TransactionResult {
    return { success: false, message: "Please select a transaction type." };
  }

  cancel(atm: ATM): void {
    console.log("Session cancelled. Ejecting card.");
    atm.resetSession();
    atm.setState(new IdleState());
  }
}

class TransactionSelectedState implements ATMState {
  insertCard(): void { console.log("Transaction in progress."); }
  enterPin(): void { console.log("Transaction in progress."); }
  selectTransaction(atm: ATM, type: TransactionType): void {
    atm.currentTransaction = type;
    console.log(`Transaction changed to: ${type}`);
  }

  confirmTransaction(atm: ATM, amount: number, targetAccount?: string): TransactionResult {
    const accountId = atm.accountId!;

    switch (atm.currentTransaction) {
      case TransactionType.BALANCE_INQUIRY: {
        const balance = atm.bankService.getBalance(accountId);
        atm.resetSession();
        atm.setState(new IdleState());
        return { success: true, message: `Balance: ${balance}`, balance };
      }

      case TransactionType.WITHDRAWAL: {
        if (!atm.cashDispenser.canDispense(amount)) {
          return { success: false, message: "ATM cannot dispense this amount." };
        }
        if (atm.bankService.withdraw(accountId, amount)) {
          const bills = atm.cashDispenser.dispense(amount);
          const balance = atm.bankService.getBalance(accountId);
          atm.resetSession();
          atm.setState(new IdleState());
          return { success: true, message: "Cash dispensed.", balance, dispensedCash: bills };
        }
        return { success: false, message: "Insufficient funds." };
      }

      case TransactionType.DEPOSIT: {
        if (atm.bankService.deposit(accountId, amount)) {
          const balance = atm.bankService.getBalance(accountId);
          atm.resetSession();
          atm.setState(new IdleState());
          return { success: true, message: `Deposited ${amount}.`, balance };
        }
        return { success: false, message: "Deposit failed." };
      }

      case TransactionType.TRANSFER: {
        if (!targetAccount) {
          return { success: false, message: "Target account required." };
        }
        if (atm.bankService.transfer(accountId, targetAccount, amount)) {
          const balance = atm.bankService.getBalance(accountId);
          atm.resetSession();
          atm.setState(new IdleState());
          return { success: true, message: `Transferred ${amount}.`, balance };
        }
        return { success: false, message: "Transfer failed." };
      }

      default:
        return { success: false, message: "Unknown transaction type." };
    }
  }

  cancel(atm: ATM): void {
    console.log("Transaction cancelled. Ejecting card.");
    atm.resetSession();
    atm.setState(new IdleState());
  }
}

Python:

python
class ATMState(ABC):
    @abstractmethod
    def insert_card(self, atm: 'ATM', card: Card) -> None: ...

    @abstractmethod
    def enter_pin(self, atm: 'ATM', pin: str) -> None: ...

    @abstractmethod
    def select_transaction(self, atm: 'ATM', tx_type: TransactionType) -> None: ...

    @abstractmethod
    def confirm_transaction(self, atm: 'ATM', amount: float,
                            target_account: str | None = None) -> TransactionResult: ...

    @abstractmethod
    def cancel(self, atm: 'ATM') -> None: ...

class IdleState(ATMState):
    def insert_card(self, atm: 'ATM', card: Card) -> None:
        if atm.bank_service.validate_card(card.card_number):
            atm.current_card = card
            atm.pin_attempts = 0
            atm.set_state(CardInsertedState())
        else:
            print("Invalid card.")

    def enter_pin(self, atm: 'ATM', pin: str) -> None:
        print("Insert a card first.")

    def select_transaction(self, atm: 'ATM', tx_type: TransactionType) -> None:
        print("Insert a card first.")

    def confirm_transaction(self, atm: 'ATM', amount: float,
                            target_account: str | None = None) -> TransactionResult:
        return TransactionResult(success=False, message="Insert a card first.")

    def cancel(self, atm: 'ATM') -> None:
        print("No active session.")

class CardInsertedState(ATMState):
    def insert_card(self, atm: 'ATM', card: Card) -> None:
        print("Card already inserted.")

    def enter_pin(self, atm: 'ATM', pin: str) -> None:
        if atm.bank_service.validate_pin(atm.current_card.card_number, pin):
            atm.account_id = atm.bank_service.get_account_id(atm.current_card.card_number)
            atm.set_state(PinEnteredState())
        else:
            atm.pin_attempts += 1
            if atm.pin_attempts >= 3:
                print("Card retained.")
                atm.reset_session()
                atm.set_state(IdleState())
            else:
                print(f"Wrong PIN. {3 - atm.pin_attempts} attempts left.")

    def select_transaction(self, atm: 'ATM', tx_type: TransactionType) -> None:
        print("Enter PIN first.")

    def confirm_transaction(self, atm: 'ATM', amount: float,
                            target_account: str | None = None) -> TransactionResult:
        return TransactionResult(success=False, message="Enter PIN first.")

    def cancel(self, atm: 'ATM') -> None:
        atm.reset_session()
        atm.set_state(IdleState())

class PinEnteredState(ATMState):
    def insert_card(self, atm: 'ATM', card: Card) -> None:
        print("Card already inserted.")

    def enter_pin(self, atm: 'ATM', pin: str) -> None:
        print("PIN already verified.")

    def select_transaction(self, atm: 'ATM', tx_type: TransactionType) -> None:
        atm.current_transaction = tx_type
        atm.set_state(TransactionSelectedState())

    def confirm_transaction(self, atm: 'ATM', amount: float,
                            target_account: str | None = None) -> TransactionResult:
        return TransactionResult(success=False, message="Select a transaction first.")

    def cancel(self, atm: 'ATM') -> None:
        atm.reset_session()
        atm.set_state(IdleState())

class TransactionSelectedState(ATMState):
    def insert_card(self, atm: 'ATM', card: Card) -> None:
        print("Transaction in progress.")

    def enter_pin(self, atm: 'ATM', pin: str) -> None:
        print("Transaction in progress.")

    def select_transaction(self, atm: 'ATM', tx_type: TransactionType) -> None:
        atm.current_transaction = tx_type

    def confirm_transaction(self, atm: 'ATM', amount: float,
                            target_account: str | None = None) -> TransactionResult:
        account_id = atm.account_id

        if atm.current_transaction == TransactionType.BALANCE_INQUIRY:
            balance = atm.bank_service.get_balance(account_id)
            atm.reset_session()
            atm.set_state(IdleState())
            return TransactionResult(True, f"Balance: {balance}", balance)

        if atm.current_transaction == TransactionType.WITHDRAWAL:
            if not atm.cash_dispenser.can_dispense(int(amount)):
                return TransactionResult(False, "Cannot dispense this amount.")
            if atm.bank_service.withdraw(account_id, amount):
                bills = atm.cash_dispenser.dispense(int(amount))
                balance = atm.bank_service.get_balance(account_id)
                atm.reset_session()
                atm.set_state(IdleState())
                return TransactionResult(True, "Cash dispensed.", balance, bills)
            return TransactionResult(False, "Insufficient funds.")

        if atm.current_transaction == TransactionType.DEPOSIT:
            if atm.bank_service.deposit(account_id, amount):
                balance = atm.bank_service.get_balance(account_id)
                atm.reset_session()
                atm.set_state(IdleState())
                return TransactionResult(True, f"Deposited {amount}.", balance)
            return TransactionResult(False, "Deposit failed.")

        if atm.current_transaction == TransactionType.TRANSFER:
            if not target_account:
                return TransactionResult(False, "Target account required.")
            if atm.bank_service.transfer(account_id, target_account, amount):
                balance = atm.bank_service.get_balance(account_id)
                atm.reset_session()
                atm.set_state(IdleState())
                return TransactionResult(True, f"Transferred {amount}.", balance)
            return TransactionResult(False, "Transfer failed.")

        return TransactionResult(False, "Unknown transaction.")

    def cancel(self, atm: 'ATM') -> None:
        atm.reset_session()
        atm.set_state(IdleState())

ATM Controller ​

TypeScript:

typescript
class ATM {
  private state: ATMState = new IdleState();
  currentCard: Card | null = null;
  accountId: string | null = null;
  currentTransaction: TransactionType | null = null;
  pinAttempts = 0;
  cashDispenser = new CashDispenser();
  bankService: BankService;

  constructor(bankService: BankService) {
    this.bankService = bankService;
  }

  setState(state: ATMState): void {
    this.state = state;
  }

  resetSession(): void {
    this.currentCard = null;
    this.accountId = null;
    this.currentTransaction = null;
    this.pinAttempts = 0;
  }

  insertCard(card: Card): void {
    this.state.insertCard(this, card);
  }

  enterPin(pin: string): void {
    this.state.enterPin(this, pin);
  }

  selectTransaction(type: TransactionType): void {
    this.state.selectTransaction(this, type);
  }

  confirmTransaction(amount = 0, targetAccount?: string): TransactionResult {
    return this.state.confirmTransaction(this, amount, targetAccount);
  }

  cancel(): void {
    this.state.cancel(this);
  }
}

Python:

python
class ATM:
    def __init__(self, bank_service: BankService):
        self._state: ATMState = IdleState()
        self.current_card: Card | None = None
        self.account_id: str | None = None
        self.current_transaction: TransactionType | None = None
        self.pin_attempts: int = 0
        self.cash_dispenser = CashDispenser()
        self.bank_service = bank_service

    def set_state(self, state: ATMState) -> None:
        self._state = state

    def reset_session(self) -> None:
        self.current_card = None
        self.account_id = None
        self.current_transaction = None
        self.pin_attempts = 0

    def insert_card(self, card: Card) -> None:
        self._state.insert_card(self, card)

    def enter_pin(self, pin: str) -> None:
        self._state.enter_pin(self, pin)

    def select_transaction(self, tx_type: TransactionType) -> None:
        self._state.select_transaction(self, tx_type)

    def confirm_transaction(self, amount: float = 0,
                            target_account: str | None = None) -> TransactionResult:
        return self._state.confirm_transaction(self, amount, target_account)

    def cancel(self) -> None:
        self._state.cancel(self)

Usage Example ​

TypeScript:

typescript
const bank = new MockBankService();
const atm = new ATM(bank);

const card: Card = {
  cardNumber: "4111111111111111",
  bankCode: "MOCK",
  expiryDate: new Date("2028-12-31"),
};

atm.insertCard(card);             // "Card accepted..."
atm.enterPin("1234");             // "PIN verified..."
atm.selectTransaction(TransactionType.WITHDRAWAL);
const result = atm.confirmTransaction(5000);
console.log(result);
// { success: true, message: "Cash dispensed.", balance: 45000, dispensedCash: ... }

Design Patterns Used ​

PatternWhere UsedWhy
StateATM states (Idle, CardInserted, etc.)Each state encapsulates valid operations and transitions
StrategyCash dispensing algorithmCould swap denomination-selection strategies
Dependency InjectionBankService injected into ATMEnables testing with mock bank service
Template MethodTransaction processing flowSame flow for all transaction types with different steps

Complexity Analysis ​

OperationTimeSpace
Card validationO(1)O(1)
PIN checkO(1)O(1)
Balance inquiryO(1)O(1)
Cash dispensingO(D) where D = number of denominationsO(D)
TransferO(1)O(1)

Common Interview Mistakes

  1. No state validation — allowing PIN entry when no card is inserted
  2. No PIN attempt limit — enabling brute-force attacks
  3. Ignoring denominations — dispensing exact amounts without considering available bills
  4. No rollback — failing midway through a transfer without reverting the withdrawal

Extensions to Discuss ​

  • Multi-language support (Strategy pattern for UI text)
  • Transaction logging and audit trail (Observer pattern)
  • ATM network management (multiple ATMs, central monitoring)
  • Denomination optimization (minimize number of bills dispensed)
  • Daily withdrawal limits per card/account

Further Reading ​

"What I cannot create, I do not understand." — Richard Feynman