~learn.

LLD 0711 min+60 XP

Design a Vending Machine.

Design a coin-operated vending machine with the State pattern, then run it

5 code

Lesson · 11 min

Four buttons, a coin slot and a return tray. The textbook problem for the State pattern, with one trap the textbooks skip: making change from the coins actually in the box.

The vending machine is the standard machine-coding problem for one pattern: State. The interviewer is checking whether you can see that "what does pressing select do?" has four different answers depending on what's happened so far, and whether you put those answers in four small classes instead of one method with a growing if/elif chain. It's also a good problem for showing judgement: the State pattern is worth it here, and you should be able to say exactly why.

This lesson builds the states, the machine they drive, and the change-making that most candidates get wrong, then runs a few customers through it.

ExplainerThe whole design: Vending machine

The State pattern: Idle, HasMoney, Dispensing and SoldOut each answer insert, select, cancel and item-dropped, while the machine holds the data and one lock. A customer inserts ₹10, ₹5 and ₹10, selects ₹20 chips, and change, stock and money are checked before the sale; coins pushed in while dispensing go to the tray. Change comes from the coins in the box: ₹6 from a ₹5 and three ₹2 coins is 2 + 2 + 2, where greedy gets stuck, and with no way to make change the machine refuses.

How the round runs#

Interviewer

"Design a vending machine. It takes ₹1, ₹2, ₹5 and ₹10 coins and ₹20 notes, sells a few products from coded slots, gives change, and lets you cancel. If it can't give change, it shouldn't sell. Make it run. 90 minutes."

A good 90 minutes
  • 0–10 min —
    clarify: denominations, change rules, cancel, what happens while dispensing, sold out
  • 10–25 min —
    draw the state diagram first: states, events, transitions
  • 25–40 min —
    products, slots, the cash box, and change-making from limited coins
  • 40–65 min —
    the State classes and the machine (the context)
  • 65–80 min —
    a demo: a normal sale, too little money, a cancel, no change possible, sold out
  • 80–90 min —
    follow-ups: UPI payments, a maintenance mode, telemetry, power loss

Requirements and clarifying questions#

You ask They answer What it changes
Which coins and notes? ₹1, ₹2, ₹5, ₹10 coins; ₹20 notes. Reject anything else. A denomination list; unknown coins go straight to the tray
Can a customer add money after selecting? Yes; selecting with too little just says how much more. Selecting in "has money" may stay in "has money"
Change from what? Only the coins in the machine, plus the ones just inserted. A cash box with counts, and bounded change-making
Can't make change? Refuse the sale and say "exact amount only". Check change before dispensing
Cancel? Return the coins that were inserted. Inserted coins wait in an escrow, separate from the cash box
Input while dispensing? Ignore buttons; return coins. A "dispensing" state that refuses everything
Your turn · checkpoint 1not answered

Draw the states before any code

List the states, and for each one say what insert coin does. Where does the machine go after the item drops?

Think it through, or jot it down. Then compare.

One strong answer

  • Idle: insert → add to balance, go to Has money
  • Has money: insert → add to balance, stay
  • Dispensing: insert → return the coin at once (the motor is running)
  • Sold out: insert → return the coin

After the item drops: back to Idle if any slot still has stock, else Sold out. Four states and four events is sixteen answers. That is the moment the State pattern pays off: each state is a class that answers all four events, and the machine never asks "which state am I in?"

DiagramVending machine: states and events
%% title: Vending machine: states and events
stateDiagram-v2
  [*] --> Idle
  Idle --> HasMoney: insert coin
  HasMoney --> HasMoney: insert coin / too little / sold-out slot
  HasMoney --> Idle: cancel, coins returned
  HasMoney --> Dispensing: select, paid, change possible
  Dispensing --> Idle: item dropped, stock left
  Dispensing --> SoldOut: item dropped, all empty
  SoldOut --> Idle: refill

The classes#

DiagramVending machine: the classes
%% title: Vending machine: the classes
classDiagram
  class VendingMachine {
    +slots dict
    +cash dict
    +escrow list
    +returned list
    +state State
    +insert(coin) str
    +select(code) str
    +cancel() str
    +item_dropped() str
    +refill(code, n)
  }
  class State {
    <<abstract>>
    +insert(m, coin) str
    +select(m, code) str
    +cancel(m) str
    +item_dropped(m) str
  }
  class Slot {
    +product Product
    +count int
  }
  class Product {
    +code str
    +name str
    +price int
  }
  VendingMachine --> State
  VendingMachine "1" *-- "many" Slot
  Slot --> Product
  State <|-- Idle
  State <|-- HasMoney
  State <|-- Dispensing
  State <|-- SoldOut

The machine is the context: it holds the data (slots, cash box, escrow, return tray) and forwards each event to its current state. Each state decides what the event means and which state comes next. The base State answers every event with a polite refusal ("coin returned", "can't select now"), so each concrete state overrides only the events it actually handles.

The interfaces#

Products, slots, and change-making. Read the docstring: with unlimited Indian coins, greedy change (largest coin first) is always optimal. With a limited cash box it isn't: to give ₹6 from one ₹5 and three ₹2 coins, greedy takes the ₹5 and then can't make ₹1, while 2 + 2 + 2 works. make_change is a small bounded-knapsack search over the coins actually present, returning the fewest coins or None.

pythonProduct · 39 lines
# vending.py, part 1: products, money, and change-making from a limited cash box
from __future__ import annotations

import threading
from abc import ABC, abstractmethod
from dataclasses import dataclass

DENOMINATIONS = (1, 2, 5, 10, 20)  # rupees: coins, plus the ₹20 note


@dataclass(frozen=True)
class Product:
    code: str
    name: str
    price: int


@dataclass
class Slot:
    product: Product
    count: int


def make_change(amount: int, cash: dict[int, int]) -> dict[int, int] | None:
    """Fewest coins that make `amount` using only what is in the box, or None.
    Greedy fails here: ₹6 from {5: 1, 2: 3} is 2 + 2 + 2, and greedy grabs the 5 first."""
    best: dict[int, dict[int, int]] = {0: {}}
    for coin, have in sorted(cash.items(), reverse=True):
        nxt = dict(best)
        for total, combo in best.items():
            for k in range(1, have + 1):
                t = total + k * coin
                if t > amount:
                    break
                c = {**combo, coin: k}
                if t not in nxt or sum(c.values()) < sum(nxt[t].values()):
                    nxt[t] = c
        best = nxt
    return best.get(amount)

The states#

Each state is a few lines. The interesting one is HasMoney.select: check the slot, check the balance, then work out the change before committing anything, counting the customer's own coins as available. Only if all three pass does it commit the sale and move to Dispensing.

pythonState · 73 lines
# part 2: the states. Each one answers every event; most answers are "not now"
class State(ABC):
    name = "?"

    def insert(self, m: VendingMachine, coin: int) -> str:
        m.returned.append(coin)
        return f"{self.name}: coin returned"

    def select(self, m: VendingMachine, code: str) -> str:
        return f"{self.name}: can't select now"

    def cancel(self, m: VendingMachine) -> str:
        return f"{self.name}: nothing to cancel"

    def item_dropped(self, m: VendingMachine) -> str:
        return f"{self.name}: ignored sensor event"


class Idle(State):
    name = "IDLE"

    def insert(self, m: VendingMachine, coin: int) -> str:
        m.escrow.append(coin)
        m.state = HasMoney()
        return f"balance ₹{m.balance}"

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


class HasMoney(State):
    name = "HAS_MONEY"

    def insert(self, m: VendingMachine, coin: int) -> str:
        m.escrow.append(coin)
        return f"balance ₹{m.balance}"

    def select(self, m: VendingMachine, code: str) -> str:
        slot = m.slots.get(code)
        if slot is None or slot.count == 0:
            return f"{code} sold out, pick another or cancel"
        if m.balance < slot.product.price:
            return f"{slot.product.name} is ₹{slot.product.price}, add ₹{slot.product.price - m.balance}"
        box = dict(m.cash)
        for c in m.escrow:  # the customer's own coins can be part of their change
            box[c] = box.get(c, 0) + 1
        change = make_change(m.balance - slot.product.price, box)
        if change is None:
            return "can't make change: exact amount only"
        m.commit(slot, box, change)
        m.state = Dispensing()
        return f"dispensing {slot.product.name}"

    def cancel(self, m: VendingMachine) -> str:
        m.returned.extend(m.escrow)  # give back the very coins that went in
        m.escrow.clear()
        m.state = Idle()
        return "cancelled, coins returned"


class Dispensing(State):
    name = "DISPENSING"

    def item_dropped(self, m: VendingMachine) -> str:
        m.state = Idle() if any(s.count for s in m.slots.values()) else SoldOut()
        return f"done, now {m.state.name}"


class SoldOut(State):
    name = "SOLD_OUT"

    def select(self, m: VendingMachine, code: str) -> str:
        return "sold out"

The patterns, and why#

Pattern Where Why it earns its place
State Idle, HasMoney, Dispensing, SoldOut Behaviour depends on state for every event. Each state is one small class; adding "maintenance mode" is a new class, not a new branch in four methods.
Context VendingMachine Holds data and the current state, forwards events, and owns the one lock. It contains no business rules.
Template default the base State refuses everything A new state handles only the events it cares about, and an unhandled event is a safe refusal, never a crash.
Strategy (optional) change-making If the operator wants "keep ₹10 coins for change", make_change becomes a strategy. Say it; don't build it unasked.

Concurrency and edge cases#

A vending machine has one customer at a time, but not one thread: the coin acceptor, the keypad, the drop sensor and a remote refill or telemetry call all fire independently. One lock in the machine serialises every event, so a refill can't change a slot's count halfway through a sale, and a coin can't land between "change computed" and "change paid out".

Edge cases
  • An unknown coin (a foreign coin, a washer): straight to the tray, no state change
  • Too little money: say how much more; stay in "has money"
  • The selected slot is empty but others aren't: say so; keep the money; let them pick again
  • No change possible: refuse the sale with "exact amount only"; never short-change silently
  • A coin inserted while dispensing: return it; the motor is running
  • The item never drops (a jam): a timeout in Dispensing that refunds the price and flags the slot
  • Power cut mid-sale: persist the state and escrow, so a restart returns the customer's coins

The code#

The machine, the context: data, the lock, and one-line forwards to the state.

pythonVendingMachine · 50 lines
# part 3: the machine (the context). It holds data; the current state decides what an event means
class VendingMachine:
    def __init__(self, slots: list[Slot], cash: dict[int, int]) -> None:
        self.slots = {s.product.code: s for s in slots}
        self.cash = dict(cash)  # coins the machine owns
        self.escrow: list[int] = []  # coins inserted for the current sale, not yet the machine's
        self.returned: list[int] = []  # what falls into the return tray
        self.state: State = Idle() if any(s.count for s in slots) else SoldOut()
        self._lock = threading.Lock()  # button presses, the coin acceptor and remote refills race

    @property
    def balance(self) -> int:
        return sum(self.escrow)

    def commit(self, slot: Slot, box: dict[int, int], change: dict[int, int]) -> None:
        slot.count -= 1
        for coin, n in change.items():
            box[coin] -= n
            self.returned.extend([coin] * n)
        self.cash = {c: n for c, n in box.items() if n}
        self.escrow.clear()

    def insert(self, coin: int) -> str:
        if coin not in DENOMINATIONS:
            self.returned.append(coin)
            return "unknown coin returned"
        with self._lock:
            return self.state.insert(self, coin)

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

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

    def item_dropped(self) -> str:  # the drop sensor saw the item fall
        with self._lock:
            return self.state.item_dropped(self)

    def refill(self, code: str, n: int) -> None:
        with self._lock:
            self.slots[code].count += n
            if isinstance(self.state, SoldOut):
                self.state = Idle()

    def take_tray(self) -> list[int]:
        out, self.returned = self.returned, []
        return out

Customers:

python22 lines
# part 4: a few customers
if __name__ == "__main__":
    vm = VendingMachine(
        [Slot(Product("A1", "Masala chips", 20), 2), Slot(Product("B2", "Mango drink", 35), 1)],
        cash={5: 1, 2: 3},
    )
    print(vm.select("A1"))  # insert money first
    print(vm.insert(10), "|", vm.insert(5), "|", vm.select("A1"))  # balance 15: add ₹5
    print(vm.insert(10), "|", vm.select("A1"))  # balance 25: dispensing, change ₹5
    print(vm.insert(10), vm.item_dropped(), vm.take_tray())  # coin returned while busy; tray [5, 10]

    print(vm.insert(20), "|", vm.insert(20), "|", vm.select("B2"))  # ₹40 for 35: change 5 from the box
    print(vm.item_dropped(), vm.take_tray())
    print(vm.insert(10), vm.select("A1"), "| cash:", vm.cash)  # 10 for 20: add ₹10
    print(vm.cancel(), vm.take_tray())  # cancelled, [10]

    vm2 = VendingMachine([Slot(Product("C3", "Biscuits", 14), 5)], cash={5: 1, 2: 3})
    print(vm2.insert(20), "|", vm2.select("C3"), vm2.take_tray())  # change ₹6 = 2+2+2; greedy takes the 5 and gets stuck
    vm3 = VendingMachine([Slot(Product("C3", "Biscuits", 14), 5)], cash={5: 1})
    print(vm3.insert(20), "|", vm3.select("C3"))  # can't make change: exact amount only
    print(vm.select("A1"), vm.insert(20), vm.select("A1"), vm.item_dropped())  # last chips: SOLD_OUT
    print(vm.insert(5), vm.take_tray())  # SOLD_OUT returns coins
text12 lines
insert money first
balance ₹10 | balance ₹15 | Masala chips is ₹20, add ₹5
balance ₹25 | dispensing Masala chips
DISPENSING: coin returned done, now IDLE [5, 10]
balance ₹20 | balance ₹40 | dispensing Mango drink
done, now IDLE [5]
balance ₹10 Masala chips is ₹20, add ₹10 | cash: {2: 3, 10: 2, 20: 2}
cancelled, coins returned [10]
balance ₹20 | dispensing Biscuits [2, 2, 2]
balance ₹20 | can't make change: exact amount only
insert money first balance ₹20 dispensing Masala chips done, now SOLD_OUT
SOLD_OUT: coin returned [5]

Follow one line at a time: ₹25 for ₹20 chips gives ₹5 change, and a coin pushed in during dispensing comes straight back. The biscuits line is the change-making trap: ₹6 change as three ₹2 coins, because greedy would have taken the only ₹5 and got stuck. The next machine has only a ₹5, so it refuses. Selling the last packet moves the machine to SOLD_OUT, which returns every coin.

Extending it: the follow-ups#

  • "Accept UPI." A PaymentMethod interface: CoinPayment (today's escrow) and UpiPayment, which shows a QR code and waits for the payment confirmation. A new AwaitingUpi state handles "paid" and "timed out". The existing states don't change.
  • "Add a maintenance mode." A Maintenance state, entered with an operator key, that refuses customers and allows refills and cash collection.
  • "Report stock and cash remotely." Observer: the machine publishes sold, sold_out and low_change events; a telemetry client sends them home.
  • "Different products, different prices by time of day." A PricingStrategy read by HasMoney.select.
  • "Survive a power cut." Persist the state name, the escrow and the slot counts after every transition, and on start-up go to Idle and return the escrow.
✗ Pitfall

Common mistakes#

What costs candidates the round
  • One VendingMachine with if self.state == "IDLE": ... elif ... in every method
  • States that hold data (the balance in HasMoney), so switching state loses money; keep data in the context
  • Greedy change from a limited box, which fails or short-changes
  • Dispensing before checking change, then discovering it can't give any
  • Putting the inserted coins straight into the cash box, so "cancel" can't return the same coins
  • New state objects with no way back: every state must have an exit for every event, even if the exit is "refuse"
Quick check4 questions · +5 XP each

Vending machine

  1. Q1/4 The machine holds one ₹5 coin and three ₹2 coins. A customer needs ₹6 change. What does greedy change-making do?

    Answer & explanation

    Answer: B

    Greedy is optimal for unlimited Indian coins but not for a limited box. The bounded search finds 2 + 2 + 2.

  2. Q2/4 In the State pattern here, where does the customer's inserted money live?

    Answer & explanation

    Answer: B

    States are behaviour; the context holds data. If the balance lived in HasMoney, moving to another state would lose it.

  3. Q3/4 A coin is inserted while the machine is in DISPENSING. What should happen?

    Answer & explanation

    Answer: C

    The motor is running and the sale is committed. The Dispensing state refuses insert by returning the coin.

  4. Q4/4 The interviewer adds a maintenance mode. What changes in this design?

    Answer & explanation

    Answer: D

    That's the point of State: a new mode is a new class answering the same events. Nothing else is edited.

End of LLD 07 · Design a Vending Machine
Finish LLD 07Stages clear as you read them (0/1 so far). Skipping ahead doesn't count: marking complete unlocks after ~2 more min of reading.

New lessons by email

New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.

Discussion · …

Public: everyone reading this lesson sees comments. Be kind and keep it on the material.

Sign in to ask a question or answer one. Reading stays open to everyone.
loading the discussion…