📌Problem Statement
Design an ATM machine system with authentication, transactions, and cash dispensing.
📌Requirements
📌Class Design
ATM States
public enum ATMState { IDLE, CARD_INSERTED, PIN_ENTERED, TRANSACTION_SELECTED, PROCESSING, DISPENSING_CASH}public interface ATMStateHandler { void insertCard(ATM atm, Card card); void enterPin(ATM atm, String pin); void selectTransaction(ATM atm, TransactionType type); void processTransaction(ATM atm, double amount); void ejectCard(ATM atm);}ATM Class
public class ATM { private String atmId; private ATMState state; private Card currentCard; private Account currentAccount; private CashDispenser cashDispenser; private Bank bank; public ATM(String atmId, Bank bank) { this.atmId = atmId; this.bank = bank; this.state = ATMState.IDLE; this.cashDispenser = new CashDispenser(); } public void insertCard(Card card) { if (state != ATMState.IDLE) { throw new InvalidStateException("ATM busy"); } this.currentCard = card; this.state = ATMState.CARD_INSERTED; display("Enter PIN"); } public boolean validatePin(String pin) { if (state != ATMState.CARD_INSERTED) { throw new InvalidStateException("Insert card first"); } boolean valid = bank.validateCard(currentCard, pin); if (valid) { currentAccount = bank.getAccount(currentCard); state = ATMState.PIN_ENTERED; return true; } return false; } public double checkBalance() { validateSession(); return currentAccount.getBalance(); } public boolean withdraw(double amount) { validateSession(); if (!cashDispenser.canDispense(amount)) { display("ATM has insufficient cash"); return false; } if (currentAccount.getBalance() < amount) { display("Insufficient balance"); return false; } state = ATMState.PROCESSING; boolean success = bank.debit(currentAccount, amount); if (success) { state = ATMState.DISPENSING_CASH; cashDispenser.dispense(amount); logTransaction(TransactionType.WITHDRAWAL, amount); return true; } return false; } public void ejectCard() { currentCard = null; currentAccount = null; state = ATMState.IDLE; } private void validateSession() { if (state != ATMState.PIN_ENTERED && state != ATMState.TRANSACTION_SELECTED) { throw new InvalidSessionException(); } }}Cash Dispenser
public class CashDispenser { private Map<Denomination, Integer> cashInventory; public CashDispenser() { cashInventory = new EnumMap<>(Denomination.class); cashInventory.put(Denomination.HUNDRED, 100); cashInventory.put(Denomination.FIVE_HUNDRED, 50); cashInventory.put(Denomination.TWO_THOUSAND, 20); } public boolean canDispense(double amount) { return getTotalCash() >= amount && amount % 100 == 0; } public Map<Denomination, Integer> dispense(double amount) { Map<Denomination, Integer> notes = new LinkedHashMap<>(); double remaining = amount; for (Denomination denom : Denomination.values()) { int available = cashInventory.get(denom); int needed = (int) (remaining / denom.getValue()); int toDispense = Math.min(available, needed); if (toDispense > 0) { notes.put(denom, toDispense); cashInventory.put(denom, available - toDispense); remaining -= toDispense * denom.getValue(); } } return notes; } public void refill(Denomination denom, int count) { cashInventory.merge(denom, count, Integer::sum); }}public enum Denomination { TWO_THOUSAND(2000), FIVE_HUNDRED(500), HUNDRED(100); private final int value; Denomination(int value) { this.value = value; } public int getValue() { return value; }}Transaction
public class Transaction { private String transactionId; private String accountId; private TransactionType type; private double amount; private LocalDateTime timestamp; private TransactionStatus status; public Transaction(String accountId, TransactionType type, double amount) { this.transactionId = UUID.randomUUID().toString(); this.accountId = accountId; this.type = type; this.amount = amount; this.timestamp = LocalDateTime.now(); this.status = TransactionStatus.PENDING; }}📌Design Patterns
Asked at Goldman Sachs, JP Morgan, and PayPal interviews.