System Design
Object-Oriented Design
ATM System

Design Example: ATM System

An Automated Teller Machine (ATM) is a complex system that requires high security, precise state management, and reliable transaction processing.


Step 1: Requirements Gathering

Core Use Cases:

  • Authentication: Insert card and verify PIN.
  • Balance Inquiry: View current account balance.
  • Withdrawal: Dispense cash and update account balance.
  • Deposit: Accept cash/checks and update balance.
  • Transfer: Move funds between linked accounts.

ATM States:

  • Idle: Waiting for a card.
  • Ready: Card inserted, waiting for PIN.
  • Processing: Authenticated, waiting for transaction choice.
  • OutOfCash: System unavailable for withdrawals.

Step 2: Identify Core Objects

  • ATM: The physical machine and its hardware controllers.
  • Card: Stores encrypted account information.
  • Account: The bank account linked to the card.
  • Transaction: Abstract base for Withdrawal, Deposit, etc.
  • CashDispenser: Hardware interface for physical cash.
  • BankService: External API for authentication and balance updates.

Step 3: Design Class Diagram


Step 4: Implementation in TypeScript

interface ATMState {
  insertCard(): void;
  enterPIN(pin: string): void;
  withdraw(amount: number): void;
}
 
class IdleState implements ATMState {
  constructor(private atm: ATM) {}
  insertCard() {
    console.log("Card inserted.");
    this.atm.setState(this.atm.getReadyState());
  }
  enterPIN(pin: string) { throw new Error("Insert card first."); }
  withdraw(amount: number) { throw new Error("Insert card first."); }
}
 
class ATM {
  private state: ATMState;
  
  constructor() {
    this.state = new IdleState(this);
  }
 
  public setState(state: ATMState) { this.state = state; }
  public getReadyState() { /* return ready state */ }
}
 
abstract class Transaction {
  constructor(protected amount: number) {}
  abstract execute(): boolean;
}
 
class Withdrawal extends Transaction {
  execute(): boolean {
    // 1. Verify balance with BankService
    // 2. Instruct CashDispenser to dispense
    // 3. Update DB
    return true;
  }
}

Deep Dive: State Pattern for ATM Lifecycle

By using the State Pattern, we eliminate complex if-else blocks for checking if a card is inserted or if the user is authenticated. Each state object only knows how to handle valid actions for its current context.


Wrap Up

Designing an ATM system tests your ability to handle Hardware-Software interaction and Transactional Integrity. Using the Command Pattern for transactions and the State Pattern for the UI flow ensures the system is both secure and maintainable.

[!CAUTION] Always ensure that hardware actions (like dispensing cash) are the final step in a transaction, only occurring after successful database updates or using a distributed transaction coordinator.