Vending Machine Design Interview: The State Machine Comes First

- State machine first: draw IDLE → HAS_MONEY → DISPENSING → RETURNING_CHANGE → OUT_OF_SERVICE before writing a single class
- State design pattern: one class per state implementing a shared interface; each state handles only its valid operations and transitions itself
- Slots over products: model physical locations with counts separately from the product catalog so restocking and inventory tracking stay accurate
- Change-making is greedy for standard denominations (US/EU coins are designed for it); switch to DP if the interviewer gives arbitrary denominations
- One mutex wraps the transaction: acquire on any state-changing call, release on return; add a 60-second auto-cancel timeout for abandoned sessions
- Fleet scope adds three problems: local-first agent syncing transactions, heartbeat for liveness detection, and threshold-based low-stock alerts
Most candidates hit "design a vending machine" and immediately start sketching microservices. An API Gateway goes up. Then a PostgreSQL cluster. Someone mentions SQS. Thirty minutes of whiteboard later, they have a distributed backend for a machine that vends Doritos.
A vending machine is a state machine. That's the insight that unlocks the whole problem. The state diagram belongs on the whiteboard before any class diagram or database schema. Draw it first and the rest of the design writes itself. Skip it and you'll spend 30 minutes fighting your own conditionals.
The vending machine design interview comes up most often as a low-level design (LLD) problem at Amazon, Uber, Walmart Labs, and similar companies. Occasionally Google or Meta extend it into a fleet management problem. The two versions share the same core but diverge fast, so clarifying which one you're solving is the first thing to do.
Clarify the Scope Before Drawing Anything
Two completely different problems share the same name here. Like two people both called "Steve" who have nothing in common. One is a quick OOD sketch, the other is a distributed systems hour.
- Design the software for a single vending machine. This is an OOD/LLD exercise. The interviewer wants to see a state machine, a clean class hierarchy, payment handling, and thread safety.
- Design a system to manage 10,000 vending machines across multiple cities. Now it's a distributed backend problem. The single-machine design becomes one component inside a larger architecture.
Ask: "Are we designing the embedded software on the machine itself, or a backend service that manages a fleet?" Then confirm payment types (cash, card, mobile), whether change needs to be tracked persistently, and whether offline operation matters. Thirty seconds of clarification saves you ten minutes of rebuilding.
The Core: Draw This Diagram First
A user walks up, inserts money, picks a product. The machine dispenses it, returns change, and resets. That flow is five states:
IDLE
↓ insertMoney()
HAS_MONEY
↓ selectProduct() [balance ≥ price, item in stock]
DISPENSING
↓ dispensed
RETURNING_CHANGE [if balance > price]
↓ change returned
IDLE
Plus two cases: calling cancel() from HAS_MONEY returns to IDLE with a full refund. Any hardware fault transitions to OUT_OF_SERVICE, which only maintenance can clear.
Draw this before your class diagram. Before your code. Before anything.
Every method is only valid in certain states, and the state machine enforces that without a wall of if/else. Calling selectProduct() from IDLE silently fails or raises an error. The state handles it. This is the State design pattern: define a VendingMachineState interface, implement one class per state, and have VendingMachine delegate to self._state. When a transition happens, the current state replaces itself.
The state machine also prevents the bugs that aren't really bugs, just bad state. You can't dispense a product with no money inserted. You can't insert money while the machine is already dispensing. The state says no and that's it.
Four Classes Own the Problem
Once the state diagram exists, the class design follows naturally.
class Product: id: str name: str price_cents: int class Slot: slot_id: str # "A1", "B3", etc. product: Product count: int capacity: int class Inventory: slots: dict[str, Slot] def is_available(self, slot_id: str) -> bool: ... def remove_item(self, slot_id: str) -> Product: ... def low_stock_slots(self, threshold: float = 0.2) -> list[Slot]: ... class PaymentProcessor: balance_cents: int def insert_money(self, amount_cents: int) -> None: ... def process_payment(self, price_cents: int) -> int: ... # returns change amount def refund(self) -> int: ...
Model slots, not just products. Diet Pepsi might sit in both A2 and C4. A slot tracks the physical location and its count. When the service tech comes to restock, they scan slot C4, not "Diet Pepsi." The Inventory class manages slots. Product is just a description.
VendingMachine itself should be a Singleton for embedded single-machine software. It holds the current state object, an Inventory, a PaymentProcessor, and the currently selected slot_id.
VendingMachine holds the current state object and delegates every action to it. The state object replaces itself on transitions.
Payment: Change-Making Is a Classic Problem in Disguise
Cash handling splits into two sub-problems. Validation (is this a real coin or bill?) is hardware-level and you can treat it as solved. Change calculation falls to you.
Returning the fewest coins for a given amount is the coin change problem. For standard denominations, greedy works: take as many of the largest coin as possible, then move to the next.
def make_change(amount_cents: int) -> dict[int, int]: denominations = [100, 25, 10, 5, 1] # dollar, quarter, dime, nickel, penny result = {} for coin in denominations: count = amount_cents // coin if count > 0: result[coin] = count amount_cents -= coin * count return result
Greedy is correct for US/EU denominations because they are specifically designed so the greedy algorithm is optimal. If the interviewer gives you arbitrary denominations, switch to dynamic programming. Mention this proactively. Nine interviews out of ten you'll see standard currency, but showing you know the difference scores points. For a full walkthrough of the DP approach, see Dynamic Programming Is Just Recursion With a Memory.
For card and mobile payments, the machine sends a payment request to a payment gateway and waits for an approved or declined response. Model this with a PaymentGateway interface and a StripeGateway or SquareGateway implementation. The state machine stays in HAS_MONEY during the network round-trip and transitions to DISPENSING on approval, back to HAS_MONEY on decline.
For a deeper look at what the backend payment layer looks like, the payment system design walkthrough covers idempotency keys and two-phase charge patterns.
Concurrency: One Lock Wraps the Transaction
A physical vending machine serializes users naturally. Only one person can stand at it at a time. You can't quantum-superpose two transactions at the coin slot. But if the machine runs a mobile payment app on a background thread alongside the physical keypad, two flows can race.
import threading class VendingMachine: def __init__(self): self._lock = threading.Lock() self._state: VendingMachineState = IdleState(self) def insert_money(self, amount_cents: int) -> bool: with self._lock: return self._state.insert_money(amount_cents) def select_product(self, slot_id: str) -> bool: with self._lock: return self._state.select_product(slot_id)
The simplest correct answer is a single mutex acquired at the start of any state-changing call and released when the call returns. The lock contention is negligible since no transaction takes more than a few seconds. Add a 60-second timeout: if money is inserted and no product is selected for a minute, auto-cancel and refund.
The payment thread blocks until the physical interaction completes. That's the entire concurrency story.
When the Interviewer Asks to Scale It
If the scope expands to a fleet, three new problems appear: How does an operator know when a machine is low on stock? How are transactions recorded centrally? What happens when a machine loses connectivity?
The machines in parking garages, stadium concourses, and basement cafeterias go offline constantly. They don't get to wait for a network handshake. They have to vend.
The data model gains two tables:
machines
id, location, address, status, last_heartbeat_at
transactions
id, machine_id, slot_id, amount_paid_cents, change_returned_cents,
payment_method, status, created_at
Each machine runs an embedded agent that records transactions locally first, then syncs to the central server. The machine's local state is always authoritative for its own transaction; the central backend is eventually consistent. This is the right split.
The agent sends a heartbeat every 60 seconds. If the server sees no heartbeat for 5 minutes, it marks the machine OFFLINE and fires an alert. Low-stock alerts fire when count / capacity drops below 20%. Both thresholds are configurable per machine group.
The local SQLite is always authoritative. The cloud is eventually consistent. That's the right tradeoff for a machine that lives in a basement.
Six Endpoints Cover the Fleet Backend
GET /machines # list all with status
GET /machines/{id}/inventory # current slot counts
POST /machines/{id}/transactions # record a completed sale (from agent sync)
PATCH /machines/{id}/slots/{slot_id} # restocking update
GET /machines/{id}/alerts # low-stock, offline, fault
GET /reports/sales # aggregate analytics by machine / region
The sync endpoint (POST /transactions) must be idempotent. The agent assigns a client_transaction_id locally and the server uses ON CONFLICT DO NOTHING on that field. Duplicate syncs on reconnect are safe.
Three Tradeoffs Worth Naming Out Loud
Local-first vs cloud-first: Local-first guarantees a transaction completes offline. The tradeoff is eventual consistency on reporting. For a machine that sells coffee in a basement, local-first is correct. A machine in a monitored retail environment might tolerate requiring connectivity.
State pattern vs if/else: A 5-state machine with if/else is readable. A 15-state machine with if/else is a debugging nightmare. The State pattern adds one class per state but makes adding MAINTENANCE_MODE or PROMOTIONAL_PRICING trivial without touching existing state classes. This is the open/closed principle applied directly.
Singleton vs instance: The Singleton is correct for embedded single-machine software. If you're building a backend service that models many machines, you want a regular class. Mention both and let the interviewer confirm scope.
For a broader look at how push vs pull tradeoffs play out in distributed systems, the tradeoff maze post works through the same tension from a different angle.
Pacing Your Vending Machine Design Interview
- 0 to 5 minutes: Clarify LLD vs fleet. Confirm payment types, offline behavior, refund policy.
- 5 to 15 minutes: State transition diagram. Walk through every state and every edge. This is the most important part of the interview.
- 15 to 30 minutes: Class diagram or code.
VendingMachine,Stateinterface,Inventory,PaymentProcessor. Implement one concrete state class and the transition. - 30 to 40 minutes: Payment handling (
make_change), the payment gateway interface, concurrency lock. - 40 to 45 minutes: If the interviewer extends scope, sketch the fleet architecture: embedded agent, sync, heartbeat, alerts.
Draw the State Diagram First
Every time. Without exception.
Candidates who skip it end up with a sprawling selectProduct() method full of conditionals. Candidates who draw it first spend 10 minutes on setup and 25 minutes on clean code.
This interview is as much about spoken reasoning as it is about code. Your interviewer is listening to how you identify states, how you handle transitions, and whether you catch the cancel/refund edge case before they have to prompt you. The code matters, but the reasoning is what gets scored.
SpaceComplexity runs voice-based mock interviews across LLD and system design problems, with follow-up questions and rubric-based feedback on exactly these signals. Try it if you want to practice the reasoning out loud before it counts.
Recap
- Draw the state transition diagram first: IDLE, HAS_MONEY, DISPENSING, RETURNING_CHANGE, OUT_OF_SERVICE
- Use the State design pattern: one class per state, each implements the same interface
- Model slots, not just products: tracks physical location and count separately from the product catalog
- Change-making is greedy for standard denominations, DP for arbitrary ones
- Concurrency: one mutex wrapping all state-changing calls, 60-second timeout for abandoned transactions
- Fleet scope: local-first embedded agent syncing to cloud, idempotent transaction records, heartbeat for liveness, threshold-based restocking alerts