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