Design a Vending Machine — Low-Level Design
Fast recall
Quick revision
Core answer
IDLE, ITEM_SELECTED, COIN_INSERTED, DISPENSING, OUT_OF_SERVICE) driven by user actions (selectItem, insertCoin, confirmPurchase, cancel). The machine holds an Inventory (item slots with quantities), a CashRegister (denominations and counts), and a PricingService. Use the State pattern so each state cleanly owns the actions that are valid in it (no giant if-else over the current state). Refunds and change use a greedy denomination algorithm with a fallback if exact change isn't available. Concurrency: one user at a time per machine — a single mutex around the state transition is fine. Persistence is optional; for a network of machines, sync inventory + register to a central service.Modeling a clean state machine with the State pattern, separating inventory / cash / pricing concerns, change-making algorithm, and edge cases (out of stock, insufficient cash, exact change unavailable, mid-transaction cancel).
Giant switch/if-else over a `currentState` field with all logic in one class; ignoring exact-change failure mode; mixing inventory and cash in the same class.
Clean State pattern with one class per state; greedy change-making with explicit fallback; observability hooks; extensibility for cashless payments and remote refill.
Interview-ready guide
Interview answer
Problem framing
Vending machine sells items for coins. Design pressure is the state machine — user goes through sequence, each action valid only in certain states.
selectItem valid in IDLE + ITEM_SELECTED. insertCoin valid in ITEM_SELECTED + COIN_INSERTED. confirmPurchase only when enough money inserted. cancel returns coins, back to IDLE.
Mixed in: inventory (item ran out), cash handling (machine ran out of small denominations for change), pricing, observability (how often each item sold, when refill needed).
Canonical small-OO problem because state-machine cleanliness is the entire grade.
Clarifying questions
Item types (catalog with codes, price, quantity). Payment (coins only? notes? cards/UPI?). Change handling (return change in available denominations? what if can't make exact?). Refund (cancel mid-transaction?). Concurrency (one user at a time per machine). Persistence (state survives restart?). Network of machines (in/out scope).
Assumptions
Coins only (5, 10, 25, 50, 100 cents). Notes/cards as extensions. Finite catalog by slot code with price + quantity. Machine returns change in available denominations; if can't, refuse purchase + refund. Mid-transaction cancel refunds. Single user at a time; one mutex. In-memory only; mention MachineRepository interface for persistence.
Core entities
VendingMachine: facade exposing selectItem, insertCoin, confirmPurchase, cancel. Holds current State, delegates.
MachineState interface: one method per user action; default impls throw InvalidActionException for actions not allowed in current state.
State impls: IdleState, ItemSelectedState, CoinInsertedState, DispensingState, OutOfServiceState.
Inventory: Map<String, Slot> by code. Slot has Item + quantity. decrement, isInStock.
CashRegister: Map<Denomination, Integer>. accept(coins), makeChange(amount) → Optional<List
State diagram
IDLE → ITEM_SELECTED (selectItem) → COIN_INSERTED (insertCoin) → DISPENSING (confirmPurchase if enough money + can make change) → IDLE.
COIN_INSERTED → COIN_INSERTED (more coins). ITEM_SELECTED → IDLE (cancel). COIN_INSERTED → IDLE (cancel + refund).
IDLE ↔ OUT_OF_SERVICE for maintenance.
Each state class enforces which transitions are legal.
Code — state pattern
MachineState interface: default methods throw 'op not allowed in this state'.
IdleState.selectItem: checks inventory.isInStock, sets selected item, transitions to ItemSelectedState.
CoinInsertedState.insertCoin: m.acceptCoin(coin) — adds to insertedCoins, increments register provisionally.
CoinInsertedState.confirmPurchase: get price, sum inserted coins. If less than price → InsufficientFundsException. Compute changeAmount. register.makeChange(changeAmount). If empty Optional → refundInsertedCoins, IdleState, ExactChangeUnavailableException. Else → DispensingState, dispense item + return change, IdleState.
CoinInsertedState.cancel: refundInsertedCoins, IdleState.
VendingMachine itself thin: synchronized methods delegate to current state.
Keeps each state's logic local — no giant switch.
Change-making algorithm
Greedy denomination works for canonical coin systems (5, 10, 25, 50, 100).
makeChange(cents): copy counts. For each denomination descending, while cents >= d and available > 0, take coin, decrement available, decrement cents. If cents != 0 at end → Optional.empty (can't make exact). Else commit, return list.
Key points:
- Try on copy first; commit only on success. Half-completed change leaves register inconsistent
- Empty result → refuse purchase with ExactChangeUnavailableException + refund
- Unusual denominations: greedy can fail when DP would succeed; mention as extension
- Refunds use same algorithm; user's just-inserted coins as hint to give back same denominations preferentially
Failure modes and edge cases
- Out of stock: selectItem throws; UI prompts another item
- Insufficient funds at confirm: stay in COIN_INSERTED; UI shows 'insert another X cents'
- Exact change unavailable: refund + abort; light 'Exact Change Only' indicator on items that can no longer give change
- Cancel mid-transaction: refund + reset
- Power loss mid-dispense: DISPENSING is dangerous. Keep small persistent log (transactionId, started, dispensed?). On reboot, if started but not dispensed — surface to operator + allow manual refund
- Item stuck in mechanism: DispensingState detects dispense failure (sensor); retry (one item over) or refund difference + mark slot needs service
- Counterfeit coin: rejected by coin acceptor before reaching insertCoin; never enters logic
- Maintenance: OutOfServiceState rejects all user actions; only operator code transitions out
Extensibility
- Cashless payment: PaymentMethod interface (requestAuthorization, capture, refund). Replace insertCoin flow with tap() that requests auth for item price. State machine gains PAYMENT_AUTHORIZING state between ITEM_SELECTED and DISPENSING
- Remote inventory monitoring: Inventory.decrement publishes ItemSoldEvent; subscriber pushes to central service for restocking dashboards
- Promotions / dynamic pricing: PricingService is extension point; no other code changes
- Multi-user kiosk: out of scope; if needed, partition state by lane with one state machine per lane
- Network of machines: each machine reports state to central service; central issues firmware + pricing updates
Trade-offs and alternatives
- State pattern vs enum+switch: State pattern is one class per state; enum+switch works for tiny machines (4 states) but bloats fast and mixes responsibilities. Default to State
- Greedy vs DP change-making: greedy correct + cheap for canonical coin systems; DP general but rarely needed
- Provisional vs deferred coin acceptance: accepting into register immediately (refund on cancel) is simpler than escrow
- In-memory vs persistent: in-memory fine with UPS / brief outage tolerance; real operator needs sales logs + inventory persisted; JournalingMachineRepository writes transitions to flash
- Mutex vs lock-free: one user per machine — single mutex is correct and simple
Recommendation
Use the State pattern with one class per state — IdleState, ItemSelectedState, CoinInsertedState, DispensingState, OutOfServiceState — so each state owns exactly the actions valid in it. Keep VendingMachine as a thin facade that delegates to the current state under a single mutex. Separate Inventory, CashRegister, and PricingService so each changes for its own reason. Use greedy change-making on a copy, commit only on success, refuse + refund when exact change isn't possible. Journal DISPENSING transitions for crash recovery. Cashless payments drop in via a PaymentMethod interface and one new state. Operational signals (ItemSoldEvent, register low, exact-change-only indicator) emerge naturally.
Design a Vending Machine — Low-Level Design
Direct Answer
Model a vending machine as a state machine (IDLE, ITEM_SELECTED, COIN_INSERTED, DISPENSING, OUT_OF_SERVICE) driven by user actions (selectItem, insertCoin, confirmPurchase, cancel). The machine holds an Inventory (item slots with quantities), a CashRegister (denominations and counts), and a PricingService. Use the State pattern so each state cleanly owns the actions that are valid in it (no giant if-else over the current state). Refunds and change use a greedy denomination algorithm with a fallback if exact change isn't available. Concurrency: one user at a time per machine — a single mutex around the state transition is fine. Persistence is optional; for a network of machines, sync inventory + register to a central service.
Common Mistakes & Pitfalls
Giant switch/if-else over a currentState field with all logic in one class; ignoring exact-change failure mode; mixing inventory and cash in the same class.
Check your understanding
Answer these without looking back at the article.
- Why is the State pattern preferred over a giant switch on `currentState`?
- Walk through your change-making logic when greedy would fail — when does that happen?
- How would you add cashless payments (card / UPI) to this design?
- How do you recover state safely after a power loss in the middle of dispensing?
Related interview prep categories
1Want a guided curriculum order?
These modules are also sequenced in our flagship backend interview track — open it when you prefer an ordered path instead of browsing by topic.