Code a snack vending machine
Open in the interactive app: practice, quiz, mock interview →In one minute
- A vending machine is a small state machine with money inside it. The design is graded on whether every operation is legal only in the right state, and whether money is never created or lost.
- Model the states (idle, has money, dispensing, out of stock) as objects that share one interface. The machine delegates every button press to its current state, so "what happens if I press select with no money" has exactly one home.
- Inserted coins sit in escrow until a sale commits. A refund returns the same coins. A sale moves escrow into the cash box, pays change out of the combined pool and decrements stock in one step.
- Change-making with a limited coin inventory is not greedy. Greedy fails when the big coin it grabs leaves a remainder it cannot pay, so use a small bounded dynamic program and refuse the sale if no exact change exists.
- The same shape (a multi-step transaction that must commit all-or-nothing and a state that gates which commands are legal) shows up in checkout flows, payment intents and order lifecycles.
Requirements
Functional
- Accept a fixed set of coin denominations (for example 5, 10, 25 and 100 cents). Reject anything else and hand it back.
- Show the current balance. Let the customer select a product by code (for example
A1). - Dispense the product and return change when the balance covers the price. Refuse the sale, keeping the balance, if the product is unknown, sold out, too expensive or if exact change cannot be made.
- Refund on request, returning the coins the customer inserted.
- Let an operator restock products and load coins for change. Stop accepting money when every slot is empty.
Non-functional
- Money is conserved: coins in equal coins out plus coins kept, for every sequence of actions.
- Operations are deterministic and finish in milliseconds on a small embedded CPU.
- A power cut in the middle of a sale must not lose the customer's credit or dispense twice.
Out of scope
- Card and mobile payments (covered under Extensibility), remote telemetry, dynamic pricing and the physical motor control loop.
Entities and relationships
- VendingMachine: the context object. It owns the inventory, the coin box, the escrow and a reference to the current state. Client code only ever talks to it.
- State: an interface with
insert_coin,selectandrefund. Four concrete states implement it:Idle,HasMoney,DispensingandOutOfStock. States are stateless singletons; the data lives in the machine. - Product slot: a code, a price in integer cents and a quantity. Prices are integers because floating-point cents drift (0.1 + 0.2 is not 0.3).
- Coin box: a count per denomination, used to pay change.
- Escrow: the list of coins the current customer has inserted but not yet spent. Keeping it separate is what makes "refund the same coins" possible.
- ChangeMaker: a pure function from (amount, coin pool) to a list of coins or "impossible". Pulling it out keeps the algorithm testable and replaceable.
The machine has a state (composition, one at a time), owns many slots and one coin box, and uses a change maker. States depend on the machine through a narrow set of methods, not the other way round.
Class diagram
classDiagram
class VendingMachine {
-State state
-Map products
-Map coins
-List escrow
+insertCoin(coin)
+select(code) Purchase
+refund() List
+addProduct(code, price, qty)
+loadCoins(coin, count)
}
class State {
<<interface>>
+insertCoin(m, coin)
+select(m, code) Purchase
+refund(m) List
}
class Idle
class HasMoney
class Dispensing
class OutOfStock
class Product {
+String code
+int price
+int qty
}
class ChangeMaker {
+makeChange(amount, pool) List
}
State <|.. Idle
State <|.. HasMoney
State <|.. Dispensing
State <|.. OutOfStock
VendingMachine o-- State
VendingMachine "1" *-- "many" Product
VendingMachine ..> ChangeMakerKey flows
sequenceDiagram
participant C as Customer
participant M as VendingMachine
participant S as HasMoney
participant K as ChangeMaker
participant D as Dispensing
C->>M: insertCoin(100)
M->>M: Idle accepts, escrow=[100], state=HasMoney
C->>M: select("A1")
M->>S: select(m, "A1")
S->>S: check known, in stock, balance >= 65
S->>K: makeChange(35, coin box + escrow)
K-->>S: [25, 10]
S->>M: state = Dispensing
S->>D: dispense(A1, [25, 10])
D->>M: commit coins, qty - 1, clear escrow
D->>M: state = Idle or OutOfStock
M-->>C: A1 and change [25, 10]The order inside select matters. Every check that can fail (unknown code, sold out, insufficient funds, no exact change) runs before any data changes. Only when all of them pass does the machine enter Dispensing and commit. A failed selection leaves the customer exactly where they were: still in HasMoney, same balance, free to choose something else or refund.
Refund is the short flow: HasMoney.refund returns the escrow list, clears it and moves to Idle. Pressing refund in Idle returns nothing and changes nothing.
Design patterns used and why
- State. Without it, every method starts with a
switchon a status enum, and adding a state means editing every switch. With it, each state is one class that answers every command, and the illegal combinations are explicit methods that raise "no money" or "out of stock". The transition table is readable by opening four small files. - Singleton states (flyweight). The states hold no data, so one shared instance per state is enough. Transitions are pointer assignments, not allocations.
- Strategy for change-making. The bounded dynamic program is the default. A machine in a country with a canonical coin system could plug in greedy, and a test can plug in a stub.
- Facade.
VendingMachineis the only public surface. The UI, the coin mechanism driver and the maintenance panel all call it, and none of them can set a state directly. - Command (optional). If you need an audit log or replay after power loss, record each accepted operation as a command object before applying it.
A common interview mistake is to put the data in the states (for example HasMoney holding the balance). Then every transition has to copy data between objects, and a bug in one copy loses money. Keep data on the context and behaviour on the states.
Concurrency and edge cases
- Single owner of the state. A real controller runs a single event loop: coin interrupts, keypad presses and the service panel all enqueue events that one thread processes in order. In a multi-threaded design, a mutex around each public method gives the same guarantee. The Go solution below does that.
- Two inputs at once. A coin lands while the customer presses select. Whichever event the loop processes first wins, and the other is handled in the new state. Because
Dispensingrejects new coins and selections, a coin that arrives mid-vend is returned, not silently added to a finished sale. - No exact change. Refuse the sale before touching stock, and display "exact change only". Some operators prefer to keep the overpayment as credit; that is a product decision, so make it a policy, not a hard-coded branch.
- Greedy trap (change-making problem). With one 25 and three 10s, change of 30 cannot start with the 25. A bounded dynamic program over the available coins finds 10+10+10.
- Coins as change. The coins just inserted are in the pool for change. A machine with an empty coin box can still sell a 50-cent item to someone who inserted three quarters.
- Dispense failure. The motor turns but the product sticks. Real machines have a drop sensor; if it does not fire within a time limit, the machine treats the sale as failed and refunds. The model should allow
Dispensingto roll back, which is why stock and coins are committed only once the vend is confirmed in a fuller design. - Restock while busy. Restocking from
HasMoneymust not reset the customer's balance. Restocking fromOutOfStockmoves toIdle.
In the wild
- Stripe, 2017: designing APIs for safe retries. A charge must happen exactly once even when a network failure leaves it unclear whether the request arrived, half-completed or succeeded with the response lost. Clients attach a unique Idempotency-Key to each POST, and the server replays the stored result instead of charging again, with retries using exponential backoff plus jitter. The same rule protects a machine that loses power between capture and dispense (Stripe blog).
- Knight Capital, 2012: a half-finished change. New code was copied to seven of eight servers, and on the eighth a reused flag woke retired trading logic that no longer tracked filled quantity. About 4 million executions in 154 stocks in roughly 45 minutes cost over $460M. Rolling back the seven good servers made all eight misbehave. The lesson for a multi-step commit is that a partly applied change is worse than none, and that the rollback path needs the same review as the change (SEC order).
Down the stack
Layer 1The contract
- After every public call:
coins_in = coins_kept + coins_returned, counted per denomination. - A failed
selectchanges nothing observable except the error shown. refundinHasMoneyreturns exactly the multiset of coins in escrow, in insertion order.- Stock never goes negative, and a sale decrements exactly one unit.
Layer 2The mechanism
The state objects are a transition table written as code. Each method either raises, mutates the context, or assigns machine.state. Change-making is a 0/1 knapsack over individual coins: best[a] holds the fewest-coin list that sums to a, and each coin is used at most once by iterating amounts downward. With 100 coins and a 200-cent amount, that is 20,000 cheap steps.
Real machines speak MDB (Multi-Drop Bus), the vending industry's serial protocol. The vending machine controller is the bus master and polls each peripheral by address: the coin changer at 0x08, cashless readers from 0x10, the bill validator at 0x30. The changer reports coins routed to its tubes, and the controller commands payouts with a dispense message. The software state machine sits on top of those messages.
Layer 3The machine
The controller is typically a microcontroller with tens of KB of RAM. MDB runs at 9600 baud with 9-bit bytes (the ninth bit marks an address byte), so one poll and reply takes a few milliseconds; the state machine is never the bottleneck, the coin mechanism is. Credit has to survive a power cut, so controllers write the balance to EEPROM or flash. Flash endures roughly 10,000 to 100,000 erase cycles per block, which is why firmware writes credit records round-robin across a region (wear levelling) instead of rewriting one cell per coin.
Why it's built this way
- Coins are physical and cannot be un-dispensed, so every check must happen before any irreversible action.
- Checks must run first, so the sale is split into validate (in
HasMoney) and commit (inDispensing). - Commit can fail mechanically, so
Dispensingexists as a separate state that refuses input until the vend is confirmed. - Coin tubes hold a limited mix of coins, so change-making must respect counts, and greedy is not safe.
- Power can fail at any instant, so credit is persisted, and the persistent write pattern follows the wear limits of flash.
Extensibility
- Card payments. Add a
Paymentabstraction withauthorize(amount)andcaptureorvoid. Cash becomes one implementation. Card payment adds anAuthorizingstate, because the authorization round trip takes seconds and can time out. - Bills. Another input device, same escrow concept. Most bill validators can hold one bill in escrow and return it, so refund still returns the same bill.
- Promotions and dynamic prices. Move price lookup behind a
PricingPolicyso a "two for one" rule does not touch the states. - Telemetry. Emit an event on each transition (observer). Operators use sales and "exact change only" events to plan restock routes.
- Multiple items per transaction. Turn the single selection into a cart;
HasMoneyaccumulates selections andDispensingvends a list. The commit rule stays the same.
Variant: coffee machine with recipes and ingredients
A coffee machine keeps the state machine and changes what a "product" is. A snack is a unit on a shelf with its own stock count. A latte is a recipe: amounts of water, beans and milk drawn from containers that every drink shares. The unit of contention moves from slots to containers, and one order must check several of them at once.
- Recipes are data. A recipe is a price plus a map of ingredient to integer amount (millilitres, grams). An option such as an extra shot is the same shape: a price delta and ingredient deltas. Do not write
Latte extends Coffee; load recipes from configuration so a new drink is a config change. - A pure plan function. Resolving an order (drink plus options) against the menu yields a
BrewPlan: the total ingredient need and the final price. Quoting and ordering share this one function, and it can reject absurd orders such as ten extra shots in one cup. - Atomic check and consume.
Inventory.canSupply(need)andconsume(need)run under one lock, so a refill or a second front end cannot slip between them. The menu shows only drinks whose plan is currently supplyable, which replaces the sold-out slot of the snack design. - Authorise, brew, capture. Cash escrow is replaced by a card hold: authorise the price, brew, then capture on success or void on failure (Stripe: place a hold shows the same two-step shape). A fault after grinding wastes beans but must not charge, so record actual consumption separately from sales and reconcile the two independently.
- Template Method for brewing. Grind, brew, add milk and dispense are a fixed sequence with variable steps, so the brewer owns the sequence and recipes only fill in parameters. Machine modes (ready, brewing, cleaning, fault) reuse the State pattern from this lesson.
- Power loss. Write a journal entry (order id, authorisation id, step) before each irreversible step. On boot, an order found authorised but not dispensed is voided; one dispensed but not captured is captured. Use stable authorisation ids so a retried call is safe (Stripe: idempotent requests).
- Low-stock alerts. An observer fires once per threshold crossing, not on every consume. Software levels are estimates, so treat a sensor or flow-meter timeout as the truth and move to a fault mode.
The problem shape is multi-step consistency over shared stock, the same as reserving several seats in one booking or filling a warehouse order from three bins. Keep money in integer minor units (see Fowler on Money) and pair it with a currency code (Stripe: currencies).
Leadership lens
- Invariant first. In design review, ask for the money-conservation invariant written as a test before the class diagram. It catches more real bugs than any pattern discussion.
- Policy versus mechanism. "Keep overpayment as credit" or "refuse without exact change" are business decisions. Make them configuration, owned by the product side, so engineering does not ship a firmware update for each market.
- Hardware boundary. The team that owns the MDB driver and the team that owns the state machine need a written contract for failure signals (jam, tube empty, coin rejected). Most field incidents happen at that boundary.
- Cost of field bugs. A bug that loses customer money on thousands of machines becomes refunds and service visits. Budget for a simulator that replays recorded event logs against new firmware before rollout.
Check yourself
Why should a failed
selectleave the machine inHasMoneyinstead of returning toIdle?Show answer
The customer still has credit in escrow. Returning to
Idlewould either lose that credit or force an implicit refund the customer did not ask for.Give a coin inventory where greedy change-making fails but an exact answer exists.
Show answer
One 25 and three 10s, with 30 to return. Greedy takes the 25 and is left needing 5; the right answer is three 10s.
Why keep the balance on the machine rather than on the
HasMoneystate object?Show answer
States should be stateless and shared. If data lived in states, every transition would copy it, and a missed copy would create or destroy money.
What does the
Dispensingstate buy you if the sale is synchronous in your code?Show answer
It is the place where a real vend waits for the drop sensor and rejects coins and selections meanwhile. It also gives you a hook to roll back on a jam without changing the other states.
Sources and further study
Primary
- Stripe: idempotency write-up: why a payment step must be safe to retry after a timeout.
- SEC order on Knight Capital: the regulator's account of a partly deployed change.
Systems docs
- Stripe: idempotent requests: how the key works in a real payment API.
- Stripe: place a hold on a payment method: authorise now, capture later, the card analogue of coin escrow.
- Stripe: currencies and minor units: why integer cents needs a currency code.
Books
- Design Patterns: Elements of Reusable Object-Oriented Software, Gamma, Helm, Johnson and Vlissides: chapter 5 (Behavioral), covering State, Observer and Template Method.
- Head First Design Patterns, Freeman and Robson, 2nd edition: chapter 10 "The State of Things: The State Pattern".
Other curricula
- Refactoring.Guru: State: the structure of the pattern and its contrast with Strategy.
- Game Programming Patterns: State: the path from boolean flags to state objects.
Watch
- State Pattern, Design Patterns ep 17 (Christopher Okhravi): a careful whiteboard derivation of the State pattern and how it differs from Strategy.
- State Design Pattern (Derek Banas): a short coded example of the pattern applied to a machine that takes money.
Free resources
- State (Refactoring.Guru): the pattern's structure, when to apply it and how it relates to Strategy.
- State (Game Programming Patterns, Robert Nystrom): a practical tour from boolean flags to finite state machines to state objects.
- Change-making problem (Wikipedia): why greedy works only for canonical coin systems and the dynamic-programming solution.
- Money (Martin Fowler, P of EAA): why money deserves its own type and should never be a float.
- Finite-state machine (Wikipedia): background on state machines, including the classic turnstile example.