Design a Vending Machine
A vending machine is a real-world-entity problem with a twist: it models a physical
device whose behavior changes depending on its current mode. The machine responds differently to a
coin insertion, a product selection, and a refund request depending on whether it currently holds
money, is dispensing, or has run out of stock. Without the State pattern, every public method
accumulates if/elif blocks — one branch per state — that grow in proportion to the number of
states. The State pattern assigns each state its own class, keeping per-state logic isolated.
See It in Action
The demo shows coin insertion, product selection, refunds, and the state transitions of the vending machine.
Clarifying Requirements
"Design a vending machine" leaves most of the domain unspecified, so raise the clarifying questions that fix it. What payment methods are supported — coins only, or also cards and mobile wallets? Does the machine hold multiple products with individual inventory counts, or is it single-product? When the customer inserts more than the item costs, does the machine return exact change? What happens if a product fails to dispense mechanically — is there a hardware-fault state? Is there an admin interface for restocking?
For this walkthrough we commit to: coins only, represented as integer cent amounts; multiple product slots each with an independent count; the machine computes and returns change on overpayment. Change is modeled as a single cent amount the machine can always return; the real-world shortfall — when the machine holds the right value but not the right coins to make exact change — is a deliberate simplification we return to in Extensions. The machine accepts additional coins while money is already in, accumulating a running balance. A product that is individually sold out can be skipped — the customer can choose another without losing their balance.
In scope: coin insertion with running balance display, product selection with dispense and
change, refund from any money-holding state, per-slot sold-out detection, and an explicit state
machine with NoMoney, HasMoney, and SoldOut states.
The high-level state machine below captures the behavior at a glance before we add any code.
Core Entities
Scanning the requirements for nouns that carry state or enforce rules yields a short list.
A Product is a value object: a product code and a price in cents. It carries no mutable state of its own, so a frozen dataclass is the right representation.
An Inventory manages the stock counts. It maps each product code to a (Product, count)
pair, exposes whether a slot is available, and decrements a count when an item is dispensed.
It also answers whether the entire machine is empty — the trigger for entering SoldOut.
A VendingMachine is the context object in State-pattern terms. It owns the current balance,
holds a reference to the current VendingMachineState, and delegates every user action to that
state. It never reads its own current state type to branch on it — the state object decides what
happens next and calls set_state to trigger a transition.
VendingMachineState is an abstract interface with three methods: insert, select, and
refund. Each concrete state implements all three, rejecting or handling each action according
to the rules of that state. The machine itself is passed as an argument so states can read the
balance, access inventory, and trigger transitions — without needing a back-reference stored at
construction time.
The four entities and their relationships at a glance:
Designing the Classes
Step 3 of the delivery framework asks you to derive each class's state (fields) and
behavior (method signatures) directly from the requirements before writing a single line of
implementation. The first fork is structural: every public method (insert, select, refund)
behaves completely differently depending on whether the machine holds money, is idle, or is sold
out. The obvious encoding is an if/elif chain on a state: str field inside each method. Before
reading on, decide whether that is the right structure here, or whether something else fits
better.
Decision checkpoint
Every public method behaves differently per mode. Model the mode as a state: str field with if/elif branches in each method, or as the State pattern with one class per state?
The State pattern is the right structure: every public method (insert, select, refund) has
completely different behavior in each state. Encoding that with if/elif in every method means
every new state forces edits in every method. With the State pattern, adding a FaultState means
writing one new class; nothing else changes.
The guiding principle throughout is Tell, Don't Ask. The machine never checks
isinstance(self._state, HasMoneyState) before deciding how to proceed. It calls
self._state.insert(self, amount) and trusts the state object to do the right thing. The state
object enforces what is legal, not the caller.
With three state classes coming, the next decision is where the mutable data lives. The running balance and the stock counts have to sit somewhere. Before reading on, decide whether each state object should own that data, or whether it belongs on the context machine that the states share.
Decision checkpoint
Should the running balance and inventory live as fields on the state objects (HasMoneyState, etc.), or on the VendingMachine context the states are handed?
Mutable data lives on the context, not the states. The balance and inventory sit on
VendingMachine; each state object carries no fields and reads what it needs from the machine
passed in.
VendingMachineState
The interface is stateless itself — it carries no fields. Its sole purpose is to give the three
concrete states a common type so VendingMachine can hold any of them behind a single reference.
Three abstract methods follow from the requirements, each accepting the machine context as a
parameter so the state can read machine.balance, machine.inventory, and call
machine.set_state(...) to trigger a transition.
insert(machine, amount) -> list[str]
select(machine, code) -> list[str]
refund(machine) -> list[str]
Passing the machine as an argument (rather than storing it at construction time) keeps each state object lightweight and stateless — they can be freely instantiated during transitions without any circular-reference bookkeeping.
NoMoneyState
This state carries no fields — the machine is idle. The three behaviors follow directly from the requirements:
insert— the first coin changes the machine's mode. Accept the amount, transition toHasMoneyState.select— no money is present; the action is rejected immediately.refund— the balance is already zero; this is a harmless no-op.
This state is also the resting state after a successful dispense or after a refund clears the balance.
HasMoneyState
This state also carries no fields — the balance lives on the machine, not the state. The three behaviors are:
insert— accumulate more money. Stay in this state; the machine already holds funds.select— the most complex method. Three guard checks must pass in order: is the code a known product? Is that slot still stocked? Is the balance sufficient? On success: compute change, zero the balance, decrement inventory. Then decide the next state: if inventory is now entirely empty, transition toSoldOutState; otherwise transition toNoMoneyState.refund— return the full balance and transition toNoMoneyState.
The ordering of guards matters. Checking code validity before availability before funds avoids giving the customer information they did not ask for (e.g., revealing that a slot is stocked when the code they typed was wrong).
SoldOutState
This state also carries no fields. Its three behaviors are minimal by design:
insert— the machine cannot accept money when nothing is for sale. The coin is returned immediately as a refund, no balance accumulates, and the state does not change.select— every slot is empty; reject all selections.refund— a balance can still exist if the machine sold out mid-session. Return it and zero the balance.
The insert behavior here returns the coin immediately rather than accumulating a balance the
machine cannot spend, because nothing is available for purchase.
Inventory
The state is a dictionary mapping each product code to a [Product, count] pair. Both the
product metadata and the quantity live together so a single lookup gives both. The methods
follow from what each use-site needs:
stock(code, price, qty)— add a new slot or add to an existing count.get_product(code)— retrieve metadata; returnsNonefor unknown codes.is_available(code)— true only if the slot exists and the count is positive.decrement(code)— reduce count by one after a successful dispense.is_empty()— true when every slot's count is zero; the trigger forSoldOutState.
Product
Its state is code (string) and price (integer cents). Both are set at creation and never
mutated — a frozen dataclass is the correct representation. Product carries no behavior beyond
construction; all decisions that involve a product (is it available? is the balance sufficient?)
live in Inventory or in the state classes that compare machine.balance against
product.price.
VendingMachine
Its mutable state is balance (integer cents) and _state (the current
VendingMachineState). The machine also owns an Inventory. The three user-facing methods are
one-liners — they exist only to delegate to the current state. set_state is the single seam
through which transitions flow; no other code reaches _state directly.
stock is the one method that does not delegate to a state object. It writes to inventory
directly (stocking is an admin action, not a state-dependent customer action), then handles
the SoldOut → NoMoney transition when the restock makes inventory non-empty again.
insert(amount) -> list[str] # delegates to _state.insert(self, amount)
select(code) -> list[str] # delegates to _state.select(self, code)
refund() -> list[str] # delegates to _state.refund(self)
stock(code, price, qty) # updates inventory; lifts SoldOut if inventory non-empty
set_state(state) # replaces _state; called only by state objects
State transitions and class relationships
The state diagram shows the three concrete states and every legal transition.
The class diagram shows the ownership and inheritance structure.
Try It Yourself
Before reading the implementation, build it. Implement a function vending_machine(instructions)
that replays a sequence of commands and returns one output line per command that produces output.
Each instruction is a list of strings:
["stock", code, price, qty] — add or restock a product slot; produces no output.
["insert", amount] — insert amount cents; produces "Balance: <n>".
["select", code] — attempt a purchase; produces one of:
"Dispensed <code>, change <n>" on success, "Insufficient funds", "Sold out", or "Invalid code".
["refund"] — cancel and return coins; produces "Refunded <n>".
Model the machine with an explicit state interface and three concrete state classes. The balance
must survive multiple insert calls and reset to zero after a dispense or refund.