Model an ATM as a state machine with pluggable transaction commands and a validation chain, communicating with a remote BankServer.
Overview
An ATM transitions through a well-defined lifecycle: Idle → CardInserted → PinEntered → TransactionInProgress → back to Idle. State pattern encapsulates the allowed operations at each stage. Command pattern models each transaction type (Withdrawal, Deposit, Transfer, BalanceInquiry) as a self-contained object, enabling logging and undo. Chain of Responsibility handles multi-step validation: card validity → PIN check → balance sufficiency → daily limit check. BankServer is an interface abstracting the remote bank, making the ATM testable without network I/O. Cash dispenser uses a variant of the Chain of Responsibility to dispense the right note denominations.
Requirements Analysis
Functional: insert/eject card, enter PIN, withdraw/deposit/transfer/check balance, dispense cash, print receipt. Non-functional: no operation permitted in wrong state (e.g., withdraw before PIN entry), extensible transaction types without modifying ATM, auditable transaction log via Command objects.
// Entities : ATM, Card, Account, BankServer, Transaction types, ATMState subtypes
// Patterns : State (ATM lifecycle), Command (transactions), Chain of Responsibility (validation)Core Classes & Relationships
ATMState interface declares insertCard(), enterPin(), selectTransaction(), and ejectCard(). Concrete states: IdleState, CardInsertedState, PinEnteredState, TransactionInProgressState. Transaction is a Command interface with execute() and getReceipt(). ValidationHandler is the abstract handler in the Chain of Responsibility with a successor reference.
public interface ATMState {
void insertCard(ATM atm, Card card);
void enterPin(ATM atm, String pin);
void selectTransaction(ATM atm, Transaction txn);
void ejectCard(ATM atm);
}
public interface Transaction {
void execute(BankServer bank, Account account);
String getReceipt();
}
public interface BankServer {
Account authenticate(Card card, String pin);
boolean debit(String accountId, double amount);
boolean credit(String accountId, double amount);
double getBalance(String accountId);
boolean transfer(String fromId, String toId, double amount);
}
public abstract class ValidationHandler {
protected ValidationHandler next;
public ValidationHandler setNext(ValidationHandler next) { this.next = next; return next; }
public abstract void validate(Card card, String pin, double amount, BankServer bank);
}Java Implementation
ATM delegates every user action to its current ATMState. IdleState only permits insertCard; other operations throw. PinEnteredState permits selectTransaction and ejectCard. WithdrawalTransaction encapsulates the debit-and-dispense logic. PinValidationHandler and BalanceValidationHandler form the validation chain.
public class ATM {
private ATMState state = new IdleState();
private Card currentCard;
private Account currentAccount;
public void setState(ATMState s) { this.state = s; }
public void setCurrentCard(Card c) { this.currentCard = c; }
public void setCurrentAccount(Account a){ this.currentAccount = a; }
public Account getCurrentAccount() { return currentAccount; }
public void insertCard(Card card) { state.insertCard(this, card); }
public void enterPin(String pin) { state.enterPin(this, pin); }
public void selectTransaction(Transaction t){ state.selectTransaction(this, t); }
public void ejectCard() { state.ejectCard(this); }
}
class IdleState implements ATMState {
public void insertCard(ATM atm, Card card) {
atm.setCurrentCard(card);
atm.setState(new CardInsertedState());
System.out.println("Card accepted. Please enter PIN.");
}
public void enterPin(ATM atm, String pin) { throw new IllegalStateException("Insert card first"); }
public void selectTransaction(ATM atm, Transaction t){ throw new IllegalStateException("Insert card first"); }
public void ejectCard(ATM atm) { System.out.println("No card inserted."); }
}
class CardInsertedState implements ATMState {
public void insertCard(ATM atm, Card card) { throw new IllegalStateException("Card already inserted"); }
public void enterPin(ATM atm, String pin) {
// In production: delegate to BankServer.authenticate()
System.out.println("PIN accepted.");
atm.setState(new PinEnteredState());
}
public void selectTransaction(ATM atm, Transaction t){ throw new IllegalStateException("Enter PIN first"); }
public void ejectCard(ATM atm) { atm.setCurrentCard(null); atm.setState(new IdleState()); }
}
class PinEnteredState implements ATMState {
public void insertCard(ATM atm, Card c) { throw new IllegalStateException("Card already inserted"); }
public void enterPin(ATM atm, String p) { throw new IllegalStateException("PIN already entered"); }
public void selectTransaction(ATM atm, Transaction txn) {
atm.setState(new TransactionInProgressState());
// validation chain would run here before execute
System.out.println("Processing transaction...");
atm.setState(new IdleState());
}
public void ejectCard(ATM atm) { atm.setCurrentCard(null); atm.setCurrentAccount(null); atm.setState(new IdleState()); }
}
class TransactionInProgressState implements ATMState {
public void insertCard(ATM a, Card c) { throw new IllegalStateException("Transaction in progress"); }
public void enterPin(ATM a, String p) { throw new IllegalStateException("Transaction in progress"); }
public void selectTransaction(ATM a, Transaction t){ throw new IllegalStateException("Transaction in progress"); }
public void ejectCard(ATM a) { throw new IllegalStateException("Transaction in progress"); }
}
// ── Command: Withdrawal ───────────────────────────────────────────
public class WithdrawalTransaction implements Transaction {
private final double amount;
private String receipt = "";
public WithdrawalTransaction(double amount) { this.amount = amount; }
@Override public void execute(BankServer bank, Account account) {
if (bank.debit(account.getId(), amount)) {
receipt = "Withdrawn: " + amount + " | New balance: " + bank.getBalance(account.getId());
} else {
throw new IllegalStateException("Insufficient funds");
}
}
@Override public String getReceipt() { return receipt; }
}Key Points to Remember
- 1State pattern eliminates nested if-else on ATM status — each state class only implements what is legal in that state.
- 2Command pattern makes transactions first-class objects: they can be logged, queued, retried, or audited independently.
- 3Chain of Responsibility for validation keeps each check (PIN, balance, daily limit) single-responsibility and reorderable.
- 4BankServer as an interface decouples the ATM from any specific bank backend, enabling unit testing with a stub.
Interview Questions
Sign in to ask AriaHow would you implement the cash-dispenser logic to minimise the number of notes dispensed?
How does the State pattern prevent invalid operations like withdrawing before PIN entry?
How would you add a "forgot PIN" flow without modifying the existing ATMState implementations?
Ask Aria about Design an ATM Machine
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.