Design an ATM system. What are the key classes and interactions?
Fast recall
Quick revision
Core answer
Whether you can decompose a real-world system into cohesive classes with clear responsibilities and model state transitions properly
Putting all logic in one ATM class instead of separating hardware components, state management, and business rules into distinct objects
Mention explicit state machine design upfront and explain why external bank communication is abstracted through BankServer interface for testability
Interview-ready guide
Interview answer
I'd open by saying an ATM is essentially a state machine, because that framing drives the whole design. The machine moves through phases — idle, card inserted, authenticated, selecting an action, and dispensing cash — and the main source of bugs is letting the wrong action happen in the wrong phase, like withdrawing before you've authenticated.
For the classes, I find the nouns: the ATM itself as the controller, Card and Account for the customer, a BankServer for external verification, a CashDispenser that manages bill inventory, and a CardReader. Then I model the phases with the State pattern. I define an ATMState interface with the operations — insertCard, enterPin, withdraw — and a concrete state class for each phase. The ATM holds a currentState reference and delegates every action to it. Each state only implements the transitions that are valid for it; anything invalid throws an IllegalStateException. So IdleState accepts a card and moves to CardInsertedState, but if you call withdraw on it, it refuses.
I'd create one ATM aggregate for each configured machine and keep responsibilities separate — the BankServer verifies the PIN and account, the CashDispenser handles the cash, the ATM just coordinates. On security I'd stress that you never store the PIN; you verify it through the bank and discard it. The big mistake to avoid is a single God class with phase flags and if-else chains everywhere — that's exactly what the State pattern cleans up.
Design an ATM system. What are the key classes and interactions
Direct Answer
An ATM system has core classes: ATM (Singleton controller), Card, Account, BankServer (external verification), CashDispenser (manages denominations), CardReader, and ATMState hierarchy (State pattern). The ATM flows through states: Idle → CardInserted → Authenticated → SelectAction → Processing → DispensingCash, with each state handling only valid transitions.
Deep Dive & Architecture
ATM aggregate and device adapters
The ATM coordinates one customer session. Hardware-facing classes such as CardReader, Keypad, Screen, and CashDispenser each hide one device. A BankGateway verifies the card and asks the bank to authorize a debit; the ATM should not own the account balance or PIN.
State controls which action is legal
The session moves through states such as Idle, CardInserted, Authenticated, and Dispensing. In Idle, inserting a card is valid but withdrawing is not. Each state handles the allowed action and returns or installs the next state, which avoids a large controller full of phase flags.
A withdrawal crosses several boundaries
For a withdrawal of 2,000, the ATM authenticates the session, asks the bank to authorize the amount, asks the dispenser to prepare an exact note plan, dispenses only after both checks succeed, records the outcome, and then clears the session. Failure paths must also be explicit: insufficient account funds, insufficient notes, a device error, timeout, or cancellation. One ATM object can represent one physical machine through normal application wiring; a global Singleton is not what protects its state.
Code Example
interface ATMState {
void insertCard(ATM atm, Card card);
void enterPin(ATM atm, char[] pin);
void withdraw(ATM atm, Money amount);
}
class ATM {
private ATMState state = new IdleState();
private final BankGateway bank;
private final CashDispenser dispenser;
ATM(BankGateway bank, CashDispenser dispenser) {
this.bank = bank;
this.dispenser = dispenser;
}
void transitionTo(ATMState next) { state = next; }
void insertCard(Card card) { state.insertCard(this, card); }
void withdraw(Money amount) { state.withdraw(this, amount); }
BankGateway bank() { return bank; }
CashDispenser dispenser() { return dispenser; }
}
class IdleState implements ATMState {
public void insertCard(ATM atm, Card card) {
atm.transitionTo(new CardInsertedState(card));
}
public void enterPin(ATM atm, char[] pin) { throw new IllegalStateException("insert card first"); }
public void withdraw(ATM atm, Money amount) { throw new IllegalStateException("authenticate first"); }
}
Application configuration creates one aggregate for each physical machine; the class does not hide a global Singleton.
Common Mistakes & Pitfalls
- Giant if-else on the phase —
if (phase == AUTH) ...everywhere. That is the State pattern done badly; make each phase a class. - One God
ATMclass doing verification, dispensing, and UI. Split outBankServerandCashDispenser(SRP). - Storing the PIN — verify via
BankServerand discard immediately; never keep it. - Ignoring invalid transitions — withdrawing before authentication must fail loudly, not silently.
Check your understanding
Answer these without looking back at the article.
- How do you model the State pattern for an ATM?
- How does the cash dispensing algorithm work in your ATM design?
- What security considerations are important in your ATM design?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.