SwadhyAI Notes

All Low-Level Design notes

State + Chain of Responsibility

ATM

An ATM is a state machine: the same keypad verbs (insert card, enter , withdraw, eject) mean different things depending on the current . A failed PIN must never dispense cash. If that logic is one ATM class with twelve booleans, a retry of an old message can spit money while Idle.

Which state am I in, and what must be true before cash leaves the machine? This is in-process machine coding: one machine, one session. A network of ATMs and a switch is high-level design (HLD). Do not draw that box. Compare with the vending machine: same idea, fewer bank rules.

Session path

1

Idle, CardIn, Authenticated, Dispensing; each owns the keypad verbs.

2

Pin ok, note multiple, balance, cassette; ordered checks before cash.

3

Snapshot and debit only. Fake it in tests; keep SQL out of Atm.

4

Physical notes. Dispense can fail; say debit-versus-cash order out loud.

What actually happens at the keypad

The twelve-booleans story above is the default interview trap. Think of the session as a hallway of rooms. Each implements the same verbs. Idle rejects withdraw. Authenticated starts a validation : card not blocked, pin already ok, amount multiple of the note, balance sufficient, cash in the . The chain is ordered. A new fraud check is a new , not a new if in Atm.withdraw().

After the pin succeeds, the session holds the card identity and a for balance and debit. The ATM must not talk SQL. A failed handler leaves the state unchanged and returns a reason. Dispensing is its own state so a second withdraw cannot start mid-cash. Pin retries are a small counter on CardIn; three failures swallow the card or eject and return to Idle. Pick one policy, write the arrow, reject the others. See State and state machines.

Simple rule of thumb

The hallway story needs a fast filter before you draw classes. Ask: which room am I in, and what must be true before cash leaves?

Prefer the when each room has real behavior for the same keypad verbs. Flags like hasCard && pinOk && dispensing look shorter until a retry sets the wrong flag and Idle dispenses. Write the state list and the forbidden arrows first; then use state objects.

How to explain this in an interview

Say: rooms own the verbs; Authenticated starts an ordered withdraw chain; BankPort is faked; one machine, not a switch network. Debit versus cassette order is named out loud.

Rooms, arrows, and who owns what

The rule of thumb names the rooms; this is the allow and reject map, then the session arrows, then the types. Each is one room. The runs only from Authenticated.

Idle

Allows

insertCard only

Rejects

withdraw and enterPin rejected; no session yet

CardIn

Allows

enterPin and eject

Rejects

withdraw blocked until pin succeeds

Authenticated

Allows

withdraw, eject, maybe balance query

Rejects

second card insert; eject first

Dispensing

Allows

complete the dispense in flight

Rejects

second withdraw and insertCard blocked

Session arrows

Diagram text: the exact Mermaid definition below preserves the nodes, labels, and connections. Enable JavaScript to view the rendered diagram.
Read the diagram definition
stateDiagram-v2
    [*] --> Idle
    Idle --> CardIn: insertCard
    CardIn --> Authenticated: pin ok
    CardIn --> Idle: eject / swallow after retries
    Authenticated --> Dispensing: withdraw chain ok
    Authenticated --> Idle: eject
    Dispensing --> Idle: cash out, eject
    note right of Idle: withdraw rejected

Responsibility walk

TypeJob
AtmATM is a : it delegates keypad verbs to the current state and holds session, , and the cash .
AtmStateEach defines what the keypad means and which transitions are forbidden.
WithdrawHandlerEach does one check and forwards. A new fraud rule is a new class in the chain.
BankPort exposes balance and debit. Fake it in tests. Keep JDBC out of the ATM core.
CassetteThe holds physical notes. Dispense is a command that can fail when empty.

Java API sketch

The rooms and table above are the same types the sketch implements: , ordered , and a on Atm.

java
interface AtmState {
  void insertCard(Atm atm, Card card);
  void enterPin(Atm atm, Pin pin);
  void withdraw(Atm atm, Money amount);
  void eject(Atm atm);
}

interface WithdrawHandler {
  void setNext(WithdrawHandler next);
  void handle(WithdrawContext ctx);
}

abstract class AbstractWithdrawHandler implements WithdrawHandler {
  private WithdrawHandler next;
  public void setNext(WithdrawHandler next) { this.next = next; }
  protected void pass(WithdrawContext ctx) {
    if (next != null) next.handle(ctx);
  }
}

class PinOkHandler extends AbstractWithdrawHandler {
  public void handle(WithdrawContext ctx) {
    if (!ctx.session.authenticated()) throw new NotAuthenticated();
    pass(ctx);
  }
}

class NoteMultipleHandler extends AbstractWithdrawHandler {
  public void handle(WithdrawContext ctx) {
    if (!ctx.amount.isMultipleOf(ctx.cassette.minNote())) throw new InvalidAmount();
    pass(ctx);
  }
}

class BalanceHandler extends AbstractWithdrawHandler {
  public void handle(WithdrawContext ctx) {
    if (ctx.account.balance().lessThan(ctx.amount)) throw new InsufficientFunds();
    pass(ctx);
  }
}

interface BankPort {
  Account snapshot(Card card);
  void debit(Card card, Money amount);
}

class Atm {
  private AtmState state = new IdleState();
  private final BankPort bank;
  void insertCard(Card card) { state.insertCard(this, card); }
  void enterPin(Pin pin) { state.enterPin(this, pin); }
  void withdraw(Money amount) { state.withdraw(this, amount); }
}

Worked example: walk the hallway, then the chain

Use the session arrows and the sketch together. Idle.withdraw throws. insertCard moves to CardIn. enterPin wrong twice; on the third failure swallow or eject (pick the arrow). Pin ok moves to Authenticated.

withdraw 70 when notes are 100s: NoteMultipleHandler fails, state stays Authenticated. withdraw 200 with balance 50: BalanceHandler fails. withdraw 200 with an empty : the last handler fails with no debit, or you debit-and-compensate; say that order out loud. Dispensing rejects a second withdraw. This is one machine, not a switch network.

Numbers to know

  • Three PIN failures: swallow or eject; CardIn owns the counter and Idle return.
  • Amount must be a multiple of the smallest note; never round down silently.
  • Failed chain leaves Authenticated unchanged; card stays in until eject.
  • One physical machine: one session thread; block withdraw until Dispensing finishes.

Invalid cases, and what still breaks

The hallway walk fails in interviews for a short list of mistakes. Each one either skips , runs the too early, or muddles bank debit versus order.

Invalid cases

  • Withdraw in Idle: no card, no session; state rejects (the twelve-booleans bug).
  • Withdraw after two wrong pins before lockout: still CardIn; chain never runs.
  • Amount not a multiple of the smallest note: NoteMultipleHandler rejects; no silent rounding.
  • Balance 500, withdraw 1000: BalanceHandler fails; stay Authenticated with card in.
  • Debit the bank then fail to dispense: say cassette-first or compensating credit out loud.
  • Insert a second card while Authenticated: illegal; eject first for one session.

Booleans and concurrency

Booleans instead of states

hasCard && pinOk && dispensing. A retry sets the wrong flag. Write the state list and the forbidden arrows. Then use when each state has real behavior.

Concurrency

One physical machine: one session thread is enough. If the bank call is async, do not accept another withdraw until Dispensing finishes. Cassette counts need a lock if two commands could overlap; they should not, if the state machine is correct.

Still fails when…

  • A retry flips flags and Idle dispenses cash without an authenticated session.
  • Dispensing accepts a second withdraw and starts two cash outs at once.
  • Debit succeeds then cassette empties with no compensating credit path.

Trade-offs

  • State objects cost classes; booleans cost illegal transitions under retries.
  • Cassette-first avoids orphan debits; bank-first needs a clear compensate path.
  • One session thread is honest; async bank calls need Dispensing as a gate.

What the follow-up changes

Once the rooms and the chain in the failure card are honest, interviewers stretch the same machine. Do not rebuild it as a network of ATMs.

  • Deposit and transfer as new commands. New verbs on Authenticated, still rejected in Idle. Not new booleans on Atm.
  • A fraud velocity check. Another in the chain, not an if inside Atm. ATM.withdraw stays unchanged.
  • After three PIN failures. Swallow or lock the card per policy. CardIn owns the counter and the transition to Idle with a swallowed flag.

is a value. Do not log it. BankPort sees it once.

Wrap-up

You have the rooms, the chain, and the invalid cases. Use the seam test, then the related notes. You are done when illegal transitions are refused (for example withdraw before auth), PIN failures lock after the configured attempts, and Money stays a value object, not a double.

Tests that prove the seams

java
@Test
void withdrawRejectedInIdle() {
  Atm atm = new Atm(fakeBank, cassette);
  assertThrows(NoSession.class, () -> atm.withdraw(Money.of(200, "INR")));
}

Withdraw in Idle

Asserts NoSession; catches the twelve-booleans retry bug early.

Debit then empty cassette

Needs compensating credit or cassette-first; say the order aloud.

Key takeaway

Twelve booleans are not a session. Idle, CardIn, Authenticated, and Dispensing are. The keypad is stable; the room changes. A withdraw chain is ordered checks. is faked. One machine, not a network of ATMs. Failed pin must not dispense.