LLD Problem

Design ATM Machine

Design an ATM system with card authentication, cash withdrawal, deposits, balance inquiry, and cash dispenser logic.

25 min readMultiple Patterns

1Requirements

Functional
  • Insert card and authenticate with PIN
  • Check balance
  • Withdraw cash
  • Deposit cash/check
  • Transfer between accounts
  • Print receipt
Non-Functional
  • Secure PIN validation
  • Transaction atomicity
  • Cash dispenser optimization
  • Daily withdrawal limits
  • Card retention after 3 wrong PINs
  • Session timeout

2Design Patterns

State Pattern

ATM states: Idle, CardInserted, Authenticated, Transaction, etc.

Chain of Responsibility

Cash dispenser: $100 → $50 → $20 → $10 notes.

Strategy Pattern

Different transaction types: Withdraw, Deposit, Transfer.

Command Pattern

Encapsulate transactions for logging and undo.

3Class Design

ATM
- state
- cashDispenser
- cardReader
- bank
+ insertCard()
+ enterPIN()
+ withdraw()
+ deposit()
ATMState
- Interface
+ insertCard()
+ enterPIN()
+ selectTransaction()
+ executeTransaction()
CashDispenser
- cashSlots[]
+ dispense()
+ canDispense()
+ addCash()
CashSlot
- denomination
- count
+ dispense()
+ setNext()
Account
- number
- balance
- type
+ withdraw()
+ deposit()
+ getBalance()
Transaction
- id
- type
- amount
- timestamp
+ execute()
+ log()

4Cash Dispenser Implementation

The cash dispenser uses Chain of Responsibility: Start with largest denomination ($100), use as many as possible, pass remaining amount to next smaller denomination ($50, $20, $10).
Cash Dispenser with Chain of Responsibility
// ========== Cash Slot (Chain Handler) ==========
abstract class CashSlot {
    protected int denomination;
    protected int count;
    protected CashSlot next;
    
    public CashSlot(int denomination, int count) {
        this.denomination = denomination;
        this.count = count;
    }
    
    public void setNext(CashSlot next) {
        this.next = next;
    }
    
    public void dispense(int amount, List<Integer> result) {
        if (amount >= denomination) {
            int notesNeeded = amount / denomination;
            int notesToDispense = Math.min(notesNeeded, count);
            
            if (notesToDispense > 0) {
                result.add(notesToDispense);
                System.out.println("Dispensing " + notesToDispense + " x $" + denomination);
                count -= notesToDispense;
                amount -= notesToDispense * denomination;
            }
        }
        
        if (next != null && amount > 0) {
            next.dispense(amount, result);
        } else if (amount > 0) {
            System.out.println("Cannot dispense remaining: $" + amount);
        }
    }
    
    public boolean canDispense(int amount) {
        int remaining = amount;
        CashSlot current = this;
        
        while (current != null && remaining > 0) {
            int notes = Math.min(remaining / current.denomination, current.count);
            remaining -= notes * current.denomination;
            current = current.next;
        }
        return remaining == 0;
    }
}

class HundredDollarSlot extends CashSlot {
    public HundredDollarSlot(int count) { super(100, count); }
}

class FiftyDollarSlot extends CashSlot {
    public FiftyDollarSlot(int count) { super(50, count); }
}

class TwentyDollarSlot extends CashSlot {
    public TwentyDollarSlot(int count) { super(20, count); }
}

class TenDollarSlot extends CashSlot {
    public TenDollarSlot(int count) { super(10, count); }
}

// ========== Cash Dispenser ==========
class CashDispenser {
    private CashSlot chain;
    
    public CashDispenser() {
        // Build the chain
        CashSlot hundred = new HundredDollarSlot(10);
        CashSlot fifty = new FiftyDollarSlot(20);
        CashSlot twenty = new TwentyDollarSlot(50);
        CashSlot ten = new TenDollarSlot(100);
        
        hundred.setNext(fifty);
        fifty.setNext(twenty);
        twenty.setNext(ten);
        
        this.chain = hundred;
    }
    
    public boolean canDispense(int amount) {
        if (amount % 10 != 0) return false;
        return chain.canDispense(amount);
    }
    
    public List<Integer> dispense(int amount) {
        List<Integer> dispensed = new ArrayList<>();
        if (canDispense(amount)) {
            chain.dispense(amount, dispensed);
        }
        return dispensed;
    }
}

// ========== Usage ==========
public class Main {
    public static void main(String[] args) {
        CashDispenser dispenser = new CashDispenser();
        
        System.out.println("Withdraw $280:");
        dispenser.dispense(280);
        // Output: 2 x $100, 1 x $50, 1 x $20, 1 x $10
        
        System.out.println("\nCan dispense $15? " + dispenser.canDispense(15));
        // Output: false (not multiple of 10)
    }
}

5Interview Follow-up Questions

Interview Follow-up Questions

Common follow-up questions interviewers ask

6Key Takeaways

1State Pattern for ATM states (Idle, CardInserted, etc.).
2Chain of Responsibility for cash dispenser.
3Strategy for different transaction types.
4Always validate before dispensing cash.
5Handle concurrency with optimistic locking.
6Consider edge cases: timeout, card retention, denomination limits.