SwadhyAI Notes

All Low-Level Design notes

Classic State pattern lab

Vending machine

A vending machine is a State-pattern lab: the same coin(), select(), and refill() buttons behave differently in Idle (waiting), HasCoin (credit in the slot), Sold (dispensing), and SoldOut (empty). Nested ifs work until each case is a page long; then each state should be its own object that owns those verbs.

coin()

Idle starts credit; HasCoin adds more; SoldOut refunds the coin immediately.

select()

Idle rejects; HasCoin may move to Sold; SoldOut rejects the choice.

refill()

SoldOut can return to Idle; tests drive refill, never setState.

What actually happens when each button flips meaning

The hero above names four rooms. Context is the vending machine itself. States are Idle, HasCoin, Sold, and SoldOut. Each implements coin, select, and refill. Transitions are the interesting part: HasCoin.select may go to Sold or back to Idle with a refund.

Inventory is a map of item to count. Money in the slot is a value object (amount plus currency). The machine should not expose setState. The current decides the next state. Tests drive coin and select; they never poke a private enum. Read State pattern and state vs status.

Simple rule of thumb

Once you know the four rooms, the trap is treating SoldOut as "HasCoin with count zero." If you stay in HasCoin and sprinkle if (count == 0), you have not used the State pattern (same verbs, different objects per room). Empty inventory should move you to SoldOut, where coin() refunds immediately.

Keep change-making small. A ChangeMaker strategy (swap how notes are returned) is a nice extra; it is not required to show you understand Idle versus HasCoin. One class, a status enum, and a 60-line select() is a state machine hiding in a switch. When each branch has its own rules, extract state objects so Idle cannot dispense.

Arrows you should be able to draw from memory

With the rule above in mind, these arrows are the design. If you can redraw them cold, the object model usually follows.

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 --> HasCoin: coin
    HasCoin --> Sold: select in stock
    HasCoin --> Idle: select sold out / refund
    HasCoin --> Idle: coin overflow refund policy
    Sold --> Idle: dispense done, stock left
    Sold --> SoldOut: last item
    Idle --> SoldOut: refill missed, stock 0
    SoldOut --> Idle: refill
    SoldOut --> SoldOut: coin refunds immediately

Responsibility walk

After the diagram, name who owns stock, credit, and each verb. The public API stays tiny: coin, select, refill only.

  1. 01

    VendingMachine

    Context: holds stock, slot Money, and current state; exposes coin, select, refill only.

  2. 02

    IdleState

    Accepts coin and becomes HasCoin; rejects select; refill restocks inventory.

  3. 03

    HasCoinState

    Adds coin; select may enter Sold or refund; does not dispense while still HasCoin.

  4. 04

    SoldState

    Dispenses, decrements stock, then Idle or SoldOut; often brief, not a long-lived UI state.

  5. 05

    SoldOutState

    coin refunds immediately; select rejects; refill can leave back to Idle.

  6. 06

    ChangeMaker (optional)

    Strategy for returning notes; optional after Idle versus HasCoin is solid.

Java API sketch

The responsibility list above maps straight onto interfaces. MachineState is the shared verb surface; Idle and HasCoin own the first transitions; VendingMachine only delegates.

java
interface MachineState {
  void coin(VendingMachine machine, Money amount);
  void select(VendingMachine machine, ItemId item);
  void refill(VendingMachine machine, ItemId item, int count);
}

class IdleState implements MachineState {
  public void coin(VendingMachine m, Money amount) {
    m.credit(amount);
    m.become(new HasCoinState());
  }
  public void select(VendingMachine m, ItemId item) {
    throw new NothingSelectedYet();
  }
}

class HasCoinState implements MachineState {
  public void select(VendingMachine m, ItemId item) {
    if (!m.stockOf(item).enough()) {
      m.refund();
      m.become(m.anyStock() ? new IdleState() : new SoldOutState());
      return;
    }
    if (m.credit().lessThan(m.priceOf(item))) throw new InsufficientCredit();
    m.become(new SoldState(item));
    m.dispense(item);
  }
}

class VendingMachine {
  private MachineState state = new IdleState();
  private final Map<ItemId, Integer> stock = new HashMap<>();
  private Money slot = Money.zero("INR");
  void coin(Money amount) { state.coin(this, amount); }
  void select(ItemId item) { state.select(this, item); }
}

What still breaks, and what follow-ups change

With the API in place, these cases are how you prove the arrows. Invalid paths first, then concurrency on one machine, then the usual interview follow-ups.

Invalid cases

  • select() in Idle

    Nothing in the slot yet; reject and never go negative on stock.

  • select sold-out item with credit

    Refund or keep credit (say which); do not enter Sold for that item.

  • coin() in SoldOut that adds to a hidden slot

    Refund immediately; SoldOut is not HasCoin with a comment.

  • Public setState(SoldOut) from a test

    Assigning SoldOut skips the refill path; drive refill in tests instead.

  • Dispense without decrementing stock

    Sold must change inventory; last item must transition to SoldOut.

  • Negative refill count

    Reject at the command edge; stock is a count invariant of zero or more.

Concurrency

Two coins and a select at once: serialize on the machine. Stock decrement is the same occupy-style race as parking spots if you allow concurrent select. A single lock on VendingMachine is an honest machine-coding answer. This is not a fleet of machines talking to a warehouse service.

What the follow-up changes

  • Multiple products and prices. Catalog as a map; HasCoin.select looks up price; a new SKU is data, not a new state.
  • ChangeMaker. Strategy for notes; Idle and HasCoin still own the buttons.
  • Cancel button. HasCoin returns to Idle with a refund; new verb, same states.

How to explain this in an interview

After you name the failure modes above, walk the seams out loud. Drive Idle with coins and select; never call setState. That is what proves the State pattern (objects owning the verbs) instead of decorating a switch.

Test 1: select() in Idle throws. Test 2: coin then select an in-stock item; stock decrements, slot empties, back to Idle (or SoldOut if last). Test 3: drain inventory, coin() refunds immediately. Test 4: refill, then coin and select work again. If any test called machine.setState(new SoldOutState()), you skipped the refill path and the pattern is decoration.

Sold as a brief state: some designs dispense inside HasCoin.select and never instantiate Sold. That is fine if you still cannot dispense from Idle. The GoF (Gang of Four) names are Idle / HasCoin / Sold / SoldOut because each button meaning flips. Pick the set, draw arrows, reject the rest.

Tests that prove the seams

java
@Test
void lastItemMovesToSoldOut() {
  VendingMachine m = new VendingMachine();
  m.refill(CHIPS, 1);
  m.coin(Money.of(50, "INR"));
  m.select(CHIPS);
  assertEquals(0, m.stockOf(CHIPS));
  assertThrows(SoldOut, () -> m.select(CHIPS));
}

HasCoin with count 0

A HasCoin with zero stock is SoldOut pretending; coin() in SoldOut must refund immediately.

Public setState

If tests skip refill and force SoldOut, they skipped the real transition; state follows actions.

Wrap-up

Close the lab with a timed checklist, related reads, and the pass criteria. Change-making can wait until Idle versus HasCoin is obvious; nested ifs that mention every SKU are the smell you are deleting.

Forty-five minutes

  1. 0–8Draw Idle, HasCoin, Sold, and SoldOut arrows before writing classes.
  2. 8–20Delegate coin, select, and refill; Idle.select must throw or reject.
  3. 20–32Last item moves to SoldOut; coin in SoldOut refunds immediately.
  4. 32–40refill returns to Idle; tests never call setState on the machine.
  5. 40–45Add ChangeMaker only after the state arrows are green.

You're done when

Idle cannot dispense: select or dispense while idle must fail or no-op as your API defines. Tests must drive refill to reach SoldOut; they must not assign SoldOut by force. Every button delegates to the current state object. Stock decrements only in Sold. A coin in SoldOut refunds immediately. Four arrows with different button meanings want objects; a vending enum with guards is a lesser cousin you should name, then reject for this lab.