Facebook Pixel

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.

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

1)

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

1)

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 to HasMoneyState.
  • 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 to SoldOutState; otherwise transition to NoMoneyState.
  • refund — return the full balance and transition to NoMoneyState.

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; returns None for 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 for SoldOutState.

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.

Invest in Yourself
Your new job is waiting. 83% of people that complete the program get a job offer. Unlock unlimited access to all content and features.
Go Pro