ATM

Design an ATM that authenticates a cardholder, then supports balance inquiry, withdrawal, deposit, and transfer. Enforce per-account daily withdrawal limits and machine cash limits.

Clarifying questions

  • Is the ATM online with the bank for every operation, or does it queue when offline?
  • How is authentication done — card + PIN, biometric, both?
  • Do daily limits live on the account or the card?
  • Does the ATM dispense in fixed denominations and what happens if it can't match the amount?
  • Do deposits credit immediately or after envelope verification?

Core requirements

  • Authenticate with card and PIN; lock after N failed attempts.
  • Show balance for the authenticated account.
  • Withdraw cash subject to account balance, daily limit, and machine cash.
  • Accept deposits (cash or check) and credit the account.
  • Transfer between linked accounts.
  • Log every transaction immutably for audit.

Canonical class breakdown

ATMHardware-facing facade coordinating card reader, keypad, cash dispenser, and deposit slot.
SessionAuthenticated user session bound to a Card; expires on logout, timeout, or completion.
CardHolds card number and links to one or more Accounts; carries per-card daily limits.
AccountBank account with balance and daily-withdrawal accumulator; mutated only via Transactions.
Transaction (abstract) — Withdrawal, Deposit, Transfer, BalanceInquiryCommand-pattern operation that validates, executes, and produces an immutable audit record.
BankService (interface)Remote bank backend — ATM never mutates Accounts directly; it asks the bank.
CashDispenserKnows on-hand denominations and computes feasible payouts; refuses unmakeable amounts.
AuthServiceVerifies PIN, tracks failed attempts, and triggers card capture/lockout policy.

Java sketchjava

// Java skeleton

enum AccountType { CHECKING, SAVINGS }
enum TxStatus { PENDING, COMMITTED, FAILED }

class Card {
    private final String cardNumber;
    private final List<String> accountIds;
    private final BigDecimal dailyLimit;
}

class Account {
    private final String accountId;
    private BigDecimal balance;
    private BigDecimal todaysWithdrawals;
    private AccountType type;
}

class Session {
    private final String sessionId;
    private final Card card;
    private final Instant startedAt;
    private final Instant expiresAt;
    public boolean isExpired() { ... }
}

interface BankService {
    BigDecimal getBalance(String accountId);
    void apply(Transaction tx); // mutates the source of truth
}

class AuthService {
    private final Map<String, Integer> failedAttempts;
    public boolean verifyPin(Card card, String pin) { ... }
    public void captureCard(Card card) { ... }
}

class CashDispenser {
    private final Map<Integer, Integer> billCounts;
    public boolean canDispense(BigDecimal amount) { ... }
    public Map<Integer, Integer> dispense(BigDecimal amount) { ... }
}

abstract class Transaction {
    protected final String txId;
    protected final String accountId;
    protected final Instant createdAt;
    protected TxStatus status;
    public abstract void execute(BankService bank);
}

class Withdrawal extends Transaction {
    private final BigDecimal amount;
    public void execute(BankService bank) { /* check limits, dispense, debit */ }
}

class Deposit extends Transaction {
    private final BigDecimal amount;
    public void execute(BankService bank) { ... }
}

class Transfer extends Transaction {
    private final String toAccountId;
    private final BigDecimal amount;
    public void execute(BankService bank) { ... }
}

class BalanceInquiry extends Transaction {
    public void execute(BankService bank) { ... }
}

class ATM {
    private final AuthService auth;
    private final BankService bank;
    private final CashDispenser dispenser;
    private Session currentSession;
    public Session login(Card c, String pin) { ... }
    public void submit(Transaction tx) { /* requires active session */ }
    public void logout() { ... }
}

Key decisions to defend

  • Transactions are Command objects so they audit, retry, and reverse uniformly — not methods on Account.
  • ATM never owns balances; it calls BankService. This keeps every operation auditable and lets the ATM be dumb.
  • Session is its own object with a TTL (Time To Live) so timeouts and concurrent-card-removal are trivially correct.
  • Daily withdrawal limit lives on the Account (or Card, per the clarifier) — not the ATM, which is stateless across sessions.
  • CashDispenser refuses unmakeable amounts up front rather than partially dispensing.

Likely follow-ups

  • · ATM loses network mid-withdrawal after dispense — how do you reconcile?
  • · Support multi-currency withdrawal.
  • · Add fraud detection that can freeze a card mid-session.

Dive deeper

ATM tests whether you keep the device a thin client and the bank the source of truth. Candidates who put balance logic in the ATM end up with the impossible 'dispensed but not debited' state. Modeling each operation as a Command also signals you understand auditability — every dollar movement should be a reified, immutable record.

All OOD prompts