Coffee Vending Machine
Design the software behind a coffee machine: pick a drink, the machine checks it has enough of every ingredient, brews, and updates its stock. The interesting part isn't the brewing - it's that "idle", "selecting a drink", and "dispensing" behave differently for the exact same button presses, and that difference has to live somewhere sane.
Requirements
Functional
- The machine offers a fixed menu of
Recipes (Espresso, Latte, Cappuccino, ...), each needing specific quantities ofIngredients (water, milk, coffee beans, sugar). - A customer selects a recipe; the machine checks every required ingredient has enough stock before starting.
- If stock is insufficient, the machine reports which ingredient is short and returns to idle - it never starts brewing halfway.
- While dispensing, the machine ignores new selections until the current drink finishes.
- Ingredient levels can be topped up by a technician at any time, including mid-brew for a different slot.
Non-functional
- Adding a new drink to the menu is a new
Recipevalue, never a newifbranch in the machine's control flow. - The set of valid actions ("press select", "press cancel") has to differ by mode without a wall of mode-checking conditionals in one method.
Design
VendingMachine holds one MachineState at a time and forwards every button press to it;
the state decides whether the press does anything and what state comes next. This is the
same State pattern shape as the traffic light in
Traffic Control - a next()-shaped transition - but
here the trigger is a customer action instead of a timer, and a transition can be rejected
(pressing "select" while Dispensing does nothing) rather than always succeeding, and
IdleState itself is the one that checks stock before ever swapping in DispensingState.
- 1The machine never decides what a button press means - it just forwards the press to whatever state is current.
- 2IdleState handles a selection; DispensingState would ignore this same call entirely.
- 3The state asks the recipe what it needs, then checks that against the machine’s stock.
- 4Enough stock: the state swaps the machine into DispensingState and starts the brew.
- 5When brewing completes, DispensingState deducts stock and swaps the machine back to idle.
Recipe and Ingredient are pure data; all the interesting behavior - "can I afford
this?", "what do I do when selected?" - is on the state objects, never on the machine.
Class diagram
Code
Design decisions
MachineStatedecides both "does this button do anything right now" and "what's next". Cramming that intoVendingMachinemeans every method starts with a mode check (if (mode == DISPENSING) return;). Pushing it onto the state meansDispensingState.selectRecipe()can simply be a no-op override - the "ignored while busy" rule is enforced by which method exists, not by a guard clause repeated everywhere.- Stock is checked once, at selection, not re-checked mid-brew. A technician topping
up a different slot mid-dispense must never affect a brew already in progress, so
IdleStatesnapshots "can I afford this recipe" before handing off toDispensingState, which only ever deducts - it never re-validates. Recipestores ingredient requirements,VendingMachinestores ingredient stock - two different maps, never merged. A recipe is fixed menu data; stock changes every time a drink is made or a technician refills. Keeping them separate means restocking never touches the menu and adding a drink never touches inventory code.- What's missing for a real system: concurrent selections (two people at a
touchscreen and a mobile app at once) need the state transition itself to be atomic,
not just the state check, and partial-refund handling (customer cancels after paying
but before brewing starts) needs a
PaymentStatestep this example skips to keep the state machine to three colors instead of five.