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.