InterviewKata
Low-level design · Problem P07 of 15 · Classic systems

Vending machine

The textbook case for the state pattern. Every action (insert, select, cancel) means something different depending on what the machine is doing, and the state pattern turns a tangle of if-statements into small classes.

1. The prompt

The prompt"Design a vending machine. Users insert coins, select a product, and get the product and their change; they can cancel and get a refund. Products can sell out."

2. Clarify the requirements

Spend the first five minutes here. Ask, then write the answers down where the interviewer can see them.

AskAssume (unless told otherwise)
Which coins?5, 10, 25 and 100 cents; anything else is rejected.
Insert after selecting?Yes: the balance accumulates until a product is paid for.
Change?Returned as an amount; making it from a coin inventory is a follow-up.

Must do

  • Accept valid coins only
  • Select a product: dispense if paid enough, else say how much more
  • Cancel: refund the balance
  • Handle sold-out slots

Must be

  • Every action valid in every state, with a sensible answer
  • Easy to add a state (e.g. maintenance)
  • Never lose or create money

Out of scope

  • Card payments
  • Restocking UI
  • Exact change from a coin inventory (follow-up)

3. The classes

The class diagram: arrows show who uses or holds whom.
ClassIts one job
VendingMachineHolds the balance, the slots and the current state; delegates every action to the state
StateThe interface: insert, select, cancel
Idle / HasMoneyOne class per state, each answering every action its own way
SlotA product, its price and how many are left

4. How it works

5. Patterns and why

PatternWhereWhy it's worth it here
StateIdle and HasMoney implement StateNo 'if state == …' anywhere; adding a Maintenance state is one new class
Validation at the edgeCoins checked once in VendingMachine.insert()No state has to repeat it
Integer centsAll moneyExact arithmetic; no rounding errors
Memory hook: "The state answers, the machine delegates"The machine forwards every action to its current state object; states decide the answer and the next state.

6. The code

States are small classes; the machine forwards each call. The bracket after each line shows the state the machine ends in.

from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass

COINS = (5, 10, 25, 100)   # cents the machine accepts


@dataclass
class Slot:
    name: str
    price: int
    count: int


class State(ABC):
    """Each state answers every action in its own way: no if/else on 'what state am I in?'."""

    @abstractmethod
    def insert(self, m: VendingMachine, cents: int) -> str: ...

    @abstractmethod
    def select(self, m: VendingMachine, code: str) -> str: ...

    @abstractmethod
    def cancel(self, m: VendingMachine) -> str: ...


class Idle(State):
    def insert(self, m: VendingMachine, cents: int) -> str:
        m.balance += cents
        m.state = HasMoney()
        return f"balance {m.money(m.balance)}"

    def select(self, m: VendingMachine, code: str) -> str:
        return "insert money first"

    def cancel(self, m: VendingMachine) -> str:
        return "nothing to refund"


class HasMoney(State):
    def insert(self, m: VendingMachine, cents: int) -> str:
        m.balance += cents
        return f"balance {m.money(m.balance)}"

    def select(self, m: VendingMachine, code: str) -> str:
        slot = m.slots.get(code)
        if slot is None:
            return f"no slot {code}"
        if slot.count == 0:
            return f"{slot.name} is sold out, pick another"
        if m.balance < slot.price:
            return f"{slot.name} costs {m.money(slot.price)}, insert {m.money(slot.price - m.balance)} more"
        slot.count -= 1
        change, m.balance = m.balance - slot.price, 0
        m.state = Idle()
        return f"dispense {slot.name}, change {m.money(change)}"

    def cancel(self, m: VendingMachine) -> str:
        refund, m.balance = m.balance, 0
        m.state = Idle()
        return f"refund {m.money(refund)}"


class VendingMachine:
    def __init__(self, slots: dict[str, Slot]) -> None:
        self.slots, self.balance = slots, 0
        self.state: State = Idle()

    def insert(self, cents: int) -> str:
        if cents not in COINS:   # checked here once, before any state sees it
            return f"rejected coin {cents}c"
        return self.state.insert(self, cents)

    def select(self, code: str) -> str:
        return self.state.select(self, code)

    def cancel(self) -> str:
        return self.state.cancel(self)

    @staticmethod
    def money(c: int) -> str:
        return f"${c // 100}.{c % 100:02d}"


if __name__ == "__main__":
    vm = VendingMachine({"A1": Slot("Water", 100, 5), "B2": Slot("Chips", 150, 2), "C3": Slot("Gum", 75, 0)})
    steps = [("select", "A1"), ("insert", 100), ("select", "B2"), ("insert", 25), ("insert", 25), ("select", "B2"),
             ("insert", 3), ("insert", 100), ("select", "C3"), ("cancel", None)]
    for action, arg in steps:
        out = vm.select(arg) if action == "select" else vm.insert(arg) if action == "insert" else vm.cancel()  # type: ignore[arg-type]
        print(f"{action} {arg if arg is not None else ''}".rstrip() + f": {out}  [{type(vm.state).__name__}]")
const COINS = new Set([5, 10, 25, 100]); // cents the machine accepts

interface Slot { name: string; price: number; count: number; }

/** Each state answers every action in its own way: no if/else on "what state am I in?". */
interface State {
  readonly name: string;
  insert(m: VendingMachine, cents: number): string;
  select(m: VendingMachine, code: string): string;
  cancel(m: VendingMachine): string;
}

const money = (c: number) => `$${Math.floor(c / 100)}.${String(c % 100).padStart(2, "0")}`;

class Idle implements State {
  readonly name = "Idle";
  insert(m: VendingMachine, cents: number): string {
    m.balance += cents;
    m.state = new HasMoney();
    return `balance ${money(m.balance)}`;
  }
  select(): string { return "insert money first"; }
  cancel(): string { return "nothing to refund"; }
}

class HasMoney implements State {
  readonly name = "HasMoney";
  insert(m: VendingMachine, cents: number): string {
    m.balance += cents;
    return `balance ${money(m.balance)}`;
  }
  select(m: VendingMachine, code: string): string {
    const slot = m.slots.get(code);
    if (!slot) return `no slot ${code}`;
    if (slot.count === 0) return `${slot.name} is sold out, pick another`;
    if (m.balance < slot.price) return `${slot.name} costs ${money(slot.price)}, insert ${money(slot.price - m.balance)} more`;
    slot.count--;
    const change = m.balance - slot.price;
    m.balance = 0;
    m.state = new Idle();
    return `dispense ${slot.name}, change ${money(change)}`;
  }
  cancel(m: VendingMachine): string {
    const refund = m.balance;
    m.balance = 0;
    m.state = new Idle();
    return `refund ${money(refund)}`;
  }
}

class VendingMachine {
  balance = 0;
  state: State = new Idle();
  constructor(readonly slots: Map<string, Slot>) {}
  insert(cents: number): string {
    if (!COINS.has(cents)) return `rejected coin ${cents}c`; // checked here once, before any state sees it
    return this.state.insert(this, cents);
  }
  select(code: string): string { return this.state.select(this, code); }
  cancel(): string { return this.state.cancel(this); }
}

const vm = new VendingMachine(new Map([["A1", { name: "Water", price: 100, count: 5 }], ["B2", { name: "Chips", price: 150, count: 2 }],
  ["C3", { name: "Gum", price: 75, count: 0 }]]));
const steps: [string, string | number | null][] = [["select", "A1"], ["insert", 100], ["select", "B2"], ["insert", 25], ["insert", 25],
  ["select", "B2"], ["insert", 3], ["insert", 100], ["select", "C3"], ["cancel", null]];
for (const [action, arg] of steps) {
  const out = action === "select" ? vm.select(arg as string) : action === "insert" ? vm.insert(arg as number) : vm.cancel();
  console.log(`${action}${arg !== null ? " " + arg : ""}: ${out}  [${vm.state.name}]`);
}
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

class Slot {
    final String name;
    final int price;
    int count;

    Slot(String name, int price, int count) {
        this.name = name;
        this.price = price;
        this.count = count;
    }
}

/** Each state answers every action in its own way: no if/else on "what state am I in?". */
interface State {
    String insert(VendingMachine m, int cents);
    String select(VendingMachine m, String code);
    String cancel(VendingMachine m);
}

class Idle implements State {
    public String insert(VendingMachine m, int cents) {
        m.balance += cents;
        m.state = new HasMoney();
        return "balance " + VendingMachine.money(m.balance);
    }

    public String select(VendingMachine m, String code) {
        return "insert money first";
    }

    public String cancel(VendingMachine m) {
        return "nothing to refund";
    }
}

class HasMoney implements State {
    public String insert(VendingMachine m, int cents) {
        m.balance += cents;
        return "balance " + VendingMachine.money(m.balance);
    }

    public String select(VendingMachine m, String code) {
        Slot slot = m.slots.get(code);
        if (slot == null) return "no slot " + code;
        if (slot.count == 0) return slot.name + " is sold out, pick another";
        if (m.balance < slot.price)
            return slot.name + " costs " + VendingMachine.money(slot.price) + ", insert " + VendingMachine.money(slot.price - m.balance) + " more";
        slot.count--;
        int change = m.balance - slot.price;
        m.balance = 0;
        m.state = new Idle();
        return "dispense " + slot.name + ", change " + VendingMachine.money(change);
    }

    public String cancel(VendingMachine m) {
        int refund = m.balance;
        m.balance = 0;
        m.state = new Idle();
        return "refund " + VendingMachine.money(refund);
    }
}

class VendingMachine {
    static final Set<Integer> COINS = Set.of(5, 10, 25, 100); // cents the machine accepts
    final Map<String, Slot> slots;
    int balance;
    State state = new Idle();

    VendingMachine(Map<String, Slot> slots) {
        this.slots = slots;
    }

    synchronized String insert(int cents) {
        if (!COINS.contains(cents)) return "rejected coin " + cents + "c"; // checked here once, before any state sees it
        return state.insert(this, cents);
    }

    synchronized String select(String code) {
        return state.select(this, code);
    }

    synchronized String cancel() {
        return state.cancel(this);
    }

    static String money(int c) {
        return "$" + (c / 100) + "." + (c % 100 < 10 ? "0" : "") + (c % 100);
    }
}

public class Main {
    public static void main(String[] args) {
        Map<String, Slot> slots = new LinkedHashMap<>();
        slots.put("A1", new Slot("Water", 100, 5));
        slots.put("B2", new Slot("Chips", 150, 2));
        slots.put("C3", new Slot("Gum", 75, 0));
        VendingMachine vm = new VendingMachine(slots);
        Object[][] steps = {{"select", "A1"}, {"insert", 100}, {"select", "B2"}, {"insert", 25}, {"insert", 25},
            {"select", "B2"}, {"insert", 3}, {"insert", 100}, {"select", "C3"}, {"cancel", null}};
        for (Object[] s : steps) {
            String action = (String) s[0];
            String out = action.equals("select") ? vm.select((String) s[1])
                    : action.equals("insert") ? vm.insert((Integer) s[1]) : vm.cancel();
            System.out.println(action + (s[1] != null ? " " + s[1] : "") + ": " + out + "  [" + vm.state.getClass().getSimpleName() + "]");
        }
    }
}

Output (the same in all three languages)

select A1: insert money first  [Idle]
insert 100: balance $1.00  [HasMoney]
select B2: Chips costs $1.50, insert $0.50 more  [HasMoney]
insert 25: balance $1.25  [HasMoney]
insert 25: balance $1.50  [HasMoney]
select B2: dispense Chips, change $0.00  [Idle]
insert 3: rejected coin 3c  [Idle]
insert 100: balance $1.00  [HasMoney]
select C3: Gum is sold out, pick another  [HasMoney]
cancel: refund $1.00  [Idle]

7. Concurrency and edge cases

8. Follow-up questions

What interviewers ask once the core works, and a short answer for each.

How would you return exact change from a coin inventory?
Keep counts per coin; compute change greedily from the largest coin, and refuse the sale (or ask for exact money) if it can't be made.
Add a maintenance mode?
A Maintenance state where insert refunds immediately and select refuses; an admin method switches to and from it.
Support card payments?
A PaymentMethod abstraction: coins and cards both produce a credited balance; the states don't change.

9. Common mistakes

A big switch on state in every methodThat's what the state pattern removes; each new state then touches every method.
Changing state before dispensingIf dispensing fails you've already taken the money.
Floats for money0.1 + 0.2 isn't 0.3. Use cents.

10. Check yourself

What does the state pattern replace?

Each state becomes a class that answers every action.

Where is the coin validation?

Validate at the edge, once.

A Maintenance state is needed. What changes?

Existing states don't need to know about it.

Your turn

  1. Add a Maintenance state (10 min). Inserts are refunded; selects refused.
  2. Add a coin inventory (15 min). Make change from real coins, or refuse the sale.