Home/Learn/Low Level Design/Design an ATM Machine

Design an ATM Machine

Intermediate
LLD Interview Problems
Java Source Code

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.

Requirements
// 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.

Java — enums & interfaces
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.

Java — core classes
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 Aria
1

How would you implement the cash-dispenser logic to minimise the number of notes dispensed?

MediumAmazon
2

How does the State pattern prevent invalid operations like withdrawing before PIN entry?

MediumGoogle
3

How would you add a "forgot PIN" flow without modifying the existing ATMState implementations?

HardMicrosoft

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.

Loading discussion…