Engineering Atlas › Low-Level Design

Code a snack vending machine

15 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

Requirements

Functional

Non-functional

Out of scope

Entities and relationships

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 ..> ChangeMaker

Key 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

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

In the wild

Down the stack

Layer 1The contract

  • After every public call: coins_in = coins_kept + coins_returned, counted per denomination.
  • A failed select changes nothing observable except the error shown.
  • refund in HasMoney returns 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

  1. Coins are physical and cannot be un-dispensed, so every check must happen before any irreversible action.
  2. Checks must run first, so the sale is split into validate (in HasMoney) and commit (in Dispensing).
  3. Commit can fail mechanically, so Dispensing exists as a separate state that refuses input until the vend is confirmed.
  4. Coin tubes hold a limited mix of coins, so change-making must respect counts, and greedy is not safe.
  5. Power can fail at any instant, so credit is persisted, and the persistent write pattern follows the wear limits of flash.

Extensibility

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.

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

Check yourself

  1. Why should a failed select leave the machine in HasMoney instead of returning to Idle?

    Show answer

    The customer still has credit in escrow. Returning to Idle would either lose that credit or force an implicit refund the customer did not ask for.

  2. 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.

  3. Why keep the balance on the machine rather than on the HasMoney state 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.

  4. What does the Dispensing state 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

Systems docs

Books

Other curricula

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →