Definition
- ATM – the terminal running as a state machine: IDLE, CARD_INSERTED, AUTHENTICATED, TRANSACTION, DISPENSING.
- Card – carries the card number and is linked to an
Account. - Account – holds a balance (
BigDecimal), supports debit and credit, tracks daily withdrawal usage. - Transaction – abstract base with polymorphic implementations:
WithdrawTransaction,DepositTransaction,BalanceInquiry. - CashDispenser – dispenses cash in available denominations, fails cleanly if it cannot make the exact amount.
- PIN validation – authenticates the cardholder; retains the card after a configured number of failed attempts.
The ATM is National Bank of India's tireless teller in a box. Rajesh inserts his card, enters his PIN (identity check), chooses withdraw five thousand, the machine confirms his account has the money and he has not exceeded his daily limit, counts out the notes, debits his account, and prints a receipt. Every step follows a strict order, like the state machine in a vending machine but for cash.
Understanding the Concepts
What are the core entities?
The core entities are ATM, Card, Account, Transaction (abstract), concrete transaction types, CashDispenser, and ReceiptPrinter.
package com.bank.atm.model;
public enum ATMState {
IDLE, CARD_INSERTED, AUTHENTICATED, TRANSACTION, DISPENSING
}
package com.bank.atm.model;
import java.math.BigDecimal;
public class Account {
private final String accountId;
private BigDecimal balance;
private BigDecimal dailyWithdrawn;
private final BigDecimal dailyLimit;
public Account(String accountId, BigDecimal balance,
BigDecimal dailyLimit) {
this.accountId = accountId;
this.balance = balance;
this.dailyLimit = dailyLimit;
this.dailyWithdrawn = BigDecimal.ZERO;
}
public void debit(BigDecimal amount) {
if (balance.compareTo(amount) < 0) {
throw new IllegalStateException("Insufficient balance");
}
BigDecimal remaining = dailyLimit.subtract(dailyWithdrawn);
if (remaining.compareTo(amount) < 0) {
throw new IllegalStateException(
"Daily limit exceeded. Remaining: " + remaining);
}
balance = balance.subtract(amount);
dailyWithdrawn = dailyWithdrawn.add(amount);
}
public void credit(BigDecimal amount) {
balance = balance.add(amount);
}
public BigDecimal getBalance() { return balance; }
public String getAccountId() { return accountId; }
}
How does the ATM state machine work?
The ATM moves through states: IDLE → CARD_INSERTED → AUTHENTICATED → TRANSACTION → DISPENSING → IDLE.
package com.bank.atm.service;
import com.bank.atm.model.*;
import java.math.BigDecimal;
public class ATMService {
private ATMState state;
private Card currentCard;
private int pinAttempts;
private static final int MAX_PIN_ATTEMPTS = 3;
public ATMService() {
this.state = ATMState.IDLE;
this.pinAttempts = 0;
}
public void insertCard(Card card) {
if (state != ATMState.IDLE) {
System.out.println("ATM is busy");
return;
}
this.currentCard = card;
this.state = ATMState.CARD_INSERTED;
this.pinAttempts = 0;
System.out.println("Card inserted: " + card.getCardNumber());
}
public boolean enterPin(String pin) {
if (state != ATMState.CARD_INSERTED) {
System.out.println("Insert card first");
return false;
}
if (currentCard.validatePin(pin)) {
state = ATMState.AUTHENTICATED;
System.out.println("PIN verified");
return true;
}
pinAttempts++;
if (pinAttempts >= MAX_PIN_ATTEMPTS) {
System.out.println("Card retained after "
+ MAX_PIN_ATTEMPTS + " failed attempts");
retainCard();
return false;
}
System.out.println("Wrong PIN. Attempts left: "
+ (MAX_PIN_ATTEMPTS - pinAttempts));
return false;
}
private void retainCard() {
currentCard = null;
state = ATMState.IDLE;
}
}
How are transactions modelled polymorphically?
Transaction types are modelled as separate classes, each knowing how to execute against the account.
package com.bank.atm.model;
public abstract class Transaction {
protected final Account account;
protected Transaction(Account account) {
this.account = account;
}
public abstract void execute();
}
package com.bank.atm.model;
import java.math.BigDecimal;
public class WithdrawTransaction extends Transaction {
private final BigDecimal amount;
private final CashDispenser dispenser;
public WithdrawTransaction(Account account, BigDecimal amount,
CashDispenser dispenser) {
super(account);
this.amount = amount;
this.dispenser = dispenser;
}
@Override
public void execute() {
// Debit first
account.debit(amount);
try {
// Then dispense
dispenser.dispense(amount);
System.out.println("Withdrawn " + amount
+ " from " + account.getAccountId());
} catch (Exception e) {
// Reverse debit if dispense fails
account.credit(amount);
System.out.println("Dispense failed, debit reversed");
throw e;
}
}
}
package com.bank.atm.model;
public class BalanceInquiry extends Transaction {
public BalanceInquiry(Account account) {
super(account);
}
@Override
public void execute() {
System.out.println("Balance: " + account.getBalance());
}
}
How does the CashDispenser work?
The dispenser works in available denominations and fails cleanly if it cannot make the exact amount. All money is BigDecimal.
package com.bank.atm.model;
import java.math.BigDecimal;
import java.util.*;
public class CashDispenser {
private final TreeMap<BigDecimal, Integer> notes;
public CashDispenser(Map<BigDecimal, Integer> notes) {
this.notes = new TreeMap<>(Collections.reverseOrder());
this.notes.putAll(notes);
}
public void dispense(BigDecimal amount) {
BigDecimal remaining = amount;
Map<BigDecimal, Integer> dispensed = new LinkedHashMap<>();
for (var entry : notes.entrySet()) {
BigDecimal denomination = entry.getKey();
int available = entry.getValue();
int needed = remaining.divideToIntegralValue(
denomination).intValue();
int use = Math.min(needed, available);
if (use > 0) {
dispensed.put(denomination, use);
remaining = remaining.subtract(
denomination.multiply(
BigDecimal.valueOf(use)));
}
}
if (remaining.compareTo(BigDecimal.ZERO) != 0) {
throw new IllegalStateException(
"Cannot dispense exact amount " + amount);
}
// Commit: reduce note inventory
dispensed.forEach((denom, count) ->
notes.merge(denom, -count, Integer::sum));
System.out.println("Dispensed: " + dispensed);
}
}
Why is debit-dispense atomicity critical?
The machine must never debit without dispensing (cheats the customer) nor dispense without debiting (cheats the bank). The strategy is: debit first, then dispense. If the physical dispense fails, immediately reverse the debit.
account.debit(amount);
try {
dispenser.dispense(amount);
} catch (Exception e) {
account.credit(amount); // reverse
throw e;
}
Real ATMs achieve this with a two-phase protocol against the host system.
How does PIN validation enforce security?
After a configured number of failed PIN attempts (typically 3), the card is retained. This prevents brute-force attacks.
What about the receipt printer?
The receipt printer is a simple collaborator that receives transaction details and formats a receipt.
package com.bank.atm.model;
import java.math.BigDecimal;
import java.time.LocalDateTime;
public class ReceiptPrinter {
public void print(String accountId, String type,
BigDecimal amount, BigDecimal balance) {
System.out.println("--- RECEIPT ---");
System.out.println("Date: " + LocalDateTime.now());
System.out.println("Account: " + accountId);
System.out.println("Type: " + type);
System.out.println("Amount: " + amount);
System.out.println("Balance: " + balance);
System.out.println("--- END ---");
}
}
What should a senior engineer know?
- The debit-and-dispense coordination is the heart of ATM correctness. Always debit first, dispense second, reverse on failure.
- All money uses
BigDecimalbecause an ATM cannot tolerate floating-point rounding. - The dispenser must track its note inventory and signal when denominations run low.
- In production, the ATM communicates with the bank's host system over a secure channel, and the host is the source of truth for the account balance.
- Transaction types as polymorphic objects keep the ATM orchestrator clean and extensible.
What are common interview follow-up questions?
- What happens if power fails during dispensing?
- How do you handle a denomination running out?
- How would you add a mini-statement transaction type?
- How does the ATM communicate with the bank's core system?
- How do you prevent replay attacks on the card?
Visual Overview
ATM State Machine:
──────────────────
IDLE ──insertCard──→ CARD_INSERTED ──enterPIN──→ AUTHENTICATED
▲ │ (3 fails) │
│ retainCard selectTransaction
│ │ │
│ ▼ ▼
│ IDLE TRANSACTION
│ │
│ debit + dispense
│ │
└──────────────── ejectCard ──────────── DISPENSING
Withdrawal Atomicity:
─────────────────────
1. account.debit(amount) ← debit first
2. dispenser.dispense(amount) ← then dispense
│
├── success → done
└── failure → account.credit(amount) ← reverse
CashDispenser Denomination Breakdown: ───────────────────────────────────── Amount: ₹5000 Available: [2000 x 5, 500 x 10, 100 x 20] Dispensed: 2 x ₹2000 + 2 x ₹500 = ₹5000
Key Takeaway
When you design an ATM, model it as a state machine coordinated with the bank's account services. The machine moves through idle, card inserted, authenticated, transaction, and dispensing. Authentication validates the entered PIN, and after a few failed attempts the card is retained for security.
Transaction types are modelled polymorphically, with separate classes for withdraw, deposit, and balance inquiry, each knowing how to execute against the account. This keeps the ATM orchestrator clean and lets you add a transaction type like mini-statement as a new class. Withdrawal specifically enforces two independent checks: the account must have sufficient balance, and the amount must fit within the remaining daily limit.
The single most important correctness concern is atomicity between debiting the account and physically dispensing cash. The machine must never debit without dispensing, which would cheat the customer, nor dispense without debiting, which would cheat the bank. The strategy is debit first, then dispense, and if the physical dispense fails, immediately reverse the debit. Real ATMs achieve this with a two-phase protocol against the host system.
All money is handled with BigDecimal because an ATM cannot tolerate rounding error, and the dispenser works in available denominations, failing cleanly when it cannot make the exact amount. The senior insight is that an ATM is a safety-critical, money-exact state machine where the debit-and-dispense coordination is the heart of correctness. This is precisely the kind of transactional integrity built into every money-movement path at National Bank of India.
Remember one sentence: an ATM is a state machine that authenticates, enforces limits, debits first, dispenses second, and reverses the debit if dispensing fails.
---