The ATM Machine is one of the most commonly asked Low Level Design problems in software engineering interviews, especially at fintech companies like Paytm, PhonePe, Razorpay, and banks like HDFC and Axis. It tests your understanding of the State design pattern, the Chain of Responsibility pattern, and your ability to model hardware-constrained systems cleanly in code.
In this complete ATM machine LLD guide, we'll go from requirements to full class diagram to working Java code — the same depth you need to clear an LLD interview round.
Why Interviewers Ask the ATM Machine LLD Problem
The ATM system is a classic State machine. An ATM can only do certain things in certain states — you cannot withdraw money before inserting a card, you cannot enter a PIN before inserting a card, and you cannot insert a card if one is already inserted. Interviewers use this problem to test:
- Can you identify when the State pattern is the right solution?
- Can you model state transitions without spaghetti
if-elseblocks? - Can you apply Chain of Responsibility for cash dispensing across denominations?
- Do you think about security — PIN lockout after 3 wrong attempts, card blocking, session timeout?
- Do you separate concerns — the ATM facade vs the dispenser subsystem vs the bank network?
Functional Requirements
- User can insert a card and authenticate with a PIN
- User can check account balance
- User can withdraw cash in available denominations
- User can deposit cash or cheque
- Card is blocked after 3 incorrect PIN attempts
- Session is terminated after user ejects the card or after timeout
- ATM maintains a cash inventory across multiple denominations (₹500, ₹200, ₹100, ₹50)
- Every transaction is logged with timestamp, amount, account, and status
Non-Functional Requirements
- Concurrent withdrawals on the same account must be handled safely (atomic balance update)
- Cash dispensing must be exact — never dispense more or less than requested
- Transaction logs must be immutable — no update or delete
- PIN validation must go to the bank network, not a local cache
Core Entities — ATM Machine LLD Class Design
- ATM — the top-level facade. Delegates to state, dispenser, and bank network
- ATMState — interface defining what actions are valid in each state
- IdleState / CardInsertedState / AuthenticatedState / DispensingState — concrete states
- Card — cardNumber, maskedPAN, associated accountId
- Account — accountNumber, balance, linkedCardNumbers
- CashDispenser — tracks denomination inventory, builds dispense plan
- NoteDispenser — abstract handler in the Chain of Responsibility
- Transaction — type (WITHDRAWAL/DEPOSIT/BALANCE), amount, status, timestamp
- BankNetwork — interface to communicate with the bank for PIN validation and balance updates
- Screen / Keypad / CardReader — hardware interface abstractions (optional depth)
Text-Based Class Diagram
ATM ├── state: ATMState ├── cashDispenser: CashDispenser ├── bankNetwork: BankNetwork ├── currentCard: Card ├── insertCard(card) ├── enterPIN(pin) ├── selectTransaction(type) ├── withdraw(amount) └── ejectCard() ATMState (interface) ├── insertCard(atm, card) ├── enterPIN(atm, pin) ├── selectTransaction(atm, type) ├── withdraw(atm, amount) └── ejectCard(atm) IdleState implements ATMState CardInsertedState implements ATMState AuthenticatedState implements ATMState DispensingCashState implements ATMState CashDispenser ├── inventory: Map<Denomination, Integer> ├── canDispense(amount): boolean └── dispense(amount): DispenseResult NoteDispenser (abstract) — Chain of Responsibility ├── next: NoteDispenser ├── setNext(handler) └── dispense(amount) [abstract] FiveHundredDispenser extends NoteDispenser TwoHundredDispenser extends NoteDispenser OneHundredDispenser extends NoteDispenser FiftyDispenser extends NoteDispenser
State Pattern Implementation — ATM LLD in Java
The State pattern eliminates complex conditional logic. Instead of checking the current state in every method, each state class handles only the actions valid in that state and throws an error for everything else.
public interface ATMState {
void insertCard(ATM atm, Card card);
void enterPIN(ATM atm, String pin);
void selectTransaction(ATM atm, TransactionType type);
void withdraw(ATM atm, double amount);
void ejectCard(ATM atm);
}
public class IdleState implements ATMState {
@Override
public void insertCard(ATM atm, Card card) {
atm.setCurrentCard(card);
atm.setState(new CardInsertedState());
System.out.println("Card inserted. Please enter your PIN.");
}
@Override
public void enterPIN(ATM atm, String pin) {
throw new IllegalStateException("Please insert card first.");
}
@Override
public void withdraw(ATM atm, double amount) {
throw new IllegalStateException("Please insert card first.");
}
@Override
public void ejectCard(ATM atm) {
System.out.println("No card inserted.");
}
}
public class CardInsertedState implements ATMState {
private int pinAttempts = 0;
@Override
public void insertCard(ATM atm, Card card) {
throw new IllegalStateException("Card already inserted.");
}
@Override
public void enterPIN(ATM atm, String pin) {
boolean valid = atm.getBankNetwork().validatePIN(atm.getCurrentCard(), pin);
if (valid) {
pinAttempts = 0;
atm.setState(new AuthenticatedState());
System.out.println("PIN verified. Select transaction.");
} else {
pinAttempts++;
if (pinAttempts >= 3) {
atm.getBankNetwork().blockCard(atm.getCurrentCard());
atm.setState(new IdleState());
System.out.println("Card blocked after 3 wrong attempts.");
} else {
System.out.println("Wrong PIN. " + (3 - pinAttempts) + " attempts left.");
}
}
}
}
public class AuthenticatedState implements ATMState {
@Override
public void withdraw(ATM atm, double amount) {
Account account = atm.getBankNetwork().getAccount(atm.getCurrentCard());
if (account.getBalance() < amount) {
System.out.println("Insufficient balance.");
return;
}
if (!atm.getCashDispenser().canDispense(amount)) {
System.out.println("ATM cannot dispense this amount.");
return;
}
atm.setState(new DispensingCashState());
atm.getCashDispenser().dispense(amount);
atm.getBankNetwork().debitAccount(account, amount);
atm.logTransaction(TransactionType.WITHDRAWAL, amount, TransactionStatus.SUCCESS);
atm.setState(new IdleState());
atm.setCurrentCard(null);
}
@Override
public void ejectCard(ATM atm) {
System.out.println("Card ejected.");
atm.setState(new IdleState());
atm.setCurrentCard(null);
}
}Chain of Responsibility — Cash Dispensing
Cash dispensing is a classic Chain of Responsibility use case. Each denomination handler tries to dispense as many notes as possible, then passes the remainder to the next handler in the chain.
public abstract class NoteDispenser {
protected NoteDispenser next;
public NoteDispenser setNext(NoteDispenser next) {
this.next = next;
return next;
}
public abstract void dispense(double amount);
}
public class FiveHundredDispenser extends NoteDispenser {
@Override
public void dispense(double amount) {
int count = (int)(amount / 500);
if (count > 0) {
System.out.println("Dispensed " + count + " x 500");
}
double remainder = amount % 500;
if (remainder > 0 && next != null) {
next.dispense(remainder);
}
}
}
public class TwoHundredDispenser extends NoteDispenser {
@Override
public void dispense(double amount) {
int count = (int)(amount / 200);
if (count > 0) {
System.out.println("Dispensed " + count + " x 200");
}
double remainder = amount % 200;
if (remainder > 0 && next != null) {
next.dispense(remainder);
}
}
}
// Wire the chain: 500 -> 200 -> 100 -> 50
public class CashDispenser {
private NoteDispenser chain;
public CashDispenser() {
NoteDispenser h500 = new FiveHundredDispenser();
NoteDispenser h200 = new TwoHundredDispenser();
NoteDispenser h100 = new OneHundredDispenser();
NoteDispenser h50 = new FiftyDispenser();
h500.setNext(h200).setNext(h100).setNext(h50);
this.chain = h500;
}
public void dispense(double amount) {
chain.dispense(amount);
}
}Key Design Decisions and Why
- State pattern over if-else: If you use a single class with
currentStateenum and if-else blocks, adding a new state (like a "MaintenanceState") requires touching every method. With the State pattern, you just add a new class — Open/Closed Principle. - Chain of Responsibility for dispensing: The alternative is one method with nested ifs per denomination. The chain makes it trivial to add a new denomination (₹2000 notes) or remove one without changing existing handlers.
- BankNetwork as an interface: The ATM doesn't talk to a specific bank's API. It talks to a
BankNetworkinterface. Any bank can plug in their implementation — this is Dependency Inversion. - Atomic balance update:
debitAccounton the BankNetwork must use a DB transaction. Two concurrent withdrawals on the same account could both read the balance as ₹1000, both approve a ₹800 withdrawal, and both deduct — leaving the account at ₹200 instead of the expected negative.
Common Follow-Up Questions in ATM LLD Interviews
- "How do you handle concurrent withdrawals from two ATMs on the same account?"— Optimistic locking on the Account row with a version column, or pessimistic locking with a SELECT FOR UPDATE. The BankNetwork interface abstracts this — the ATM itself doesn't need to know.
- "What if the ATM runs out of a denomination mid-dispense?"— Each NoteDispenser checks its local inventory count before dispensing. If it can't fulfil its portion, it throws an InsufficientCashException which rolls back the entire transaction.
- "How would you add a mini-statement feature?"— Add a
printMiniStatement(atm)method to ATMState. In IdleState and CardInsertedState it throws; in AuthenticatedState it fetches the last 5 transactions from the BankNetwork. - "How do you handle session timeout?"— A scheduled task or a timer in ATM resets state to IdleState after 60 seconds of inactivity in CardInsertedState or AuthenticatedState.
- "How would you log every transaction?"— Observer pattern on the Account. Every debit/credit fires a TransactionEvent. A TransactionLogger subscriber persists it to an immutable audit log table.
What Companies Ask This Problem
The ATM machine LLD problem is most common at fintech companies — Paytm, PhonePe, Razorpay, CRED, BankBazaar — and occasionally at product companies with payments teams like Amazon Pay and Google Pay. It is also a standard problem at banking technology firms.
FAQ — ATM Machine Low Level Design
What design patterns are used in ATM machine LLD?
Two main patterns: the State pattern to model the ATM's lifecycle (Idle → Card Inserted → Authenticated → Dispensing → Idle), and the Chain of Responsibility pattern to handle cash dispensing across denominations (₹500 → ₹200 → ₹100 → ₹50). The Facade patternis also present — the ATM class acts as a facade over the card reader, dispenser, and bank network subsystems.
How do you handle wrong PIN in ATM LLD?
Maintain a pinAttempts counter in CardInsertedState. On each wrong PIN, increment the counter. When it reaches 3, call bankNetwork.blockCard(card) and transition back toIdleState. The card is now flagged in the bank's system and will be rejected on next insertion.
What is the difference between ATM LLD and ATM HLD?
LLD (Low Level Design) focuses on class structure — State pattern, class diagram, method signatures, Java/Python code. HLD (High Level Design) focuses on the distributed system — how the ATM network communicates with the bank's core system, switch routing, ISO 8583 message format, and how to handle network failures between the ATM and the bank.
How do you dispense exact cash amounts in ATM LLD?
Use the Chain of Responsibility pattern. Each denomination handler computes how many notes of its denomination fit in the remaining amount, dispenses them, and passes the remainder down the chain. Before dispensing,CashDispenser.canDispense(amount) validates that the current inventory can fulfil the request exactly.