Vending Machine#
Problem statement (interviewer prompt)
Design the software for a vending machine: insert coins / scan card, browse + select an item, validate stock, vend, return change. Model the state machine (Idle / Selecting / Dispensing / Refunding), inventory, payment methods, and the maintenance interface.
classDiagram
class VendingMachine {
+select(item)
+pay(amount)
+dispense()
+cancel()
}
class Inventory
class CashRegister
class State
VendingMachine --> Inventory
VendingMachine --> CashRegister
VendingMachine --> State
classDiagram
direction TB
class VendingMachine {
-inv: Inventory
-reg: CashRegister
-state: VMState
+select(slot)
+insertCash(c: Coin)
+cardPay(amount)
+cancel()
+dispense()
}
class VMState {
<<interface>>
+select(vm, slot)
+pay(vm, amount)
+cancel(vm)
+dispense(vm)
}
class IdleState
class SelectedState
class PaidState
class DispensingState
class OutOfServiceState
class Item {
+sku
+name
+price
}
class Inventory {
+stock: Map slot -> List Item
+reserve(slot)
+dispense(slot)
+reload(items)
}
class CashRegister {
-balance: Map Denom -> int
+acceptCoin(c)
+makeChange(amount) List~Coin~
+canMakeChange(amount) bool
}
class PaymentMethod {
<<interface>>
+pay(amount) bool
}
class CashPayment
class CardPayment
class UPIPayment
class Display
class CoinReturn
class Slot
VendingMachine --> VMState
VMState <|.. IdleState
VMState <|.. SelectedState
VMState <|.. PaidState
VMState <|.. DispensingState
VMState <|.. OutOfServiceState
VendingMachine *-- Inventory
VendingMachine *-- CashRegister
VendingMachine *-- Display
VendingMachine *-- CoinReturn
Inventory *-- "many" Slot
Slot *-- "many" Item
VendingMachine --> PaymentMethod
PaymentMethod <|.. CashPayment
PaymentMethod <|.. CardPayment
PaymentMethod <|.. UPIPayment
Vending machine state machine#
The whole interview hinges on the state machine, because every button press, coin drop, and hardware sensor must be valid for exactly one current state. Modelling the lifecycle as an explicit VMState interface (instead of nested ifs on a status enum) is what unlocks the State pattern and stops the VendingMachine god-object from sprouting dozens of conditional branches. The diagram below shows the happy path along with the four error states an interviewer will probe: InvalidCoin, OutOfStock, InsufficientFunds, and ProductJammed.
stateDiagram-v2
direction TB
[*] --> Idle
Idle --> CoinInserted: insertCoin valid
Idle --> InvalidCoin: insertCoin rejected
InvalidCoin --> Idle: coin returned
CoinInserted --> CoinInserted: insertCoin more
CoinInserted --> ProductSelected: selectProduct in-stock
CoinInserted --> OutOfStock: selectProduct empty
CoinInserted --> ChangeReturned: cancel
OutOfStock --> CoinInserted: pick another
ProductSelected --> InsufficientFunds: price gt deposited
InsufficientFunds --> CoinInserted: insert more
InsufficientFunds --> ChangeReturned: cancel
ProductSelected --> ProductDispensed: vend ok
ProductSelected --> ProductJammed: motor stall
ProductJammed --> ChangeReturned: refund issued
ProductDispensed --> ChangeReturned: change due
ProductDispensed --> Idle: no change due
ChangeReturned --> Idle: coins collected
Three transitions in this diagram carry the most interview weight. First, the CoinInserted -> CoinInserted self-loop encodes that multiple coins are summed into a running deposit before any product is selected; the running total lives on a Session object, not on the state itself, so cancelling at any point returns the full deposit. Second, the ProductSelected -> ProductJammed -> ChangeReturned chain is the one path where the user has paid but received nothing; the refund must include the full deposit (not just change) and the slot is locked until maintenance clears it. Third, ProductDispensed -> Idle skips the change step only when the running deposit equals the product price to the last paisa, which is rare in cash flows but common with card or UPI payments.
State pattern in Java#
The sketch below pins down the VendingState interface and three concrete states. The VendingMachine context simply forwards every user event to state.method(this, arg), and each state decides which transition to fire by calling vm.setState(...). New states (for example MaintenanceState for restock or OutOfServiceState for a power-on self-test failure) plug in without touching existing code.
public interface VendingState {
void insertCoin(VendingMachine vm, Coin coin);
void selectProduct(VendingMachine vm, String slot);
void dispense(VendingMachine vm);
void cancel(VendingMachine vm);
}
public final class IdleState implements VendingState {
public void insertCoin(VendingMachine vm, Coin coin) {
if (!vm.register().isValid(coin)) {
vm.coinReturn().eject(coin);
return; // stay in Idle
}
vm.session().add(coin);
vm.setState(new HasMoneyState());
}
public void selectProduct(VendingMachine vm, String slot) { /* ignored */ }
public void dispense(VendingMachine vm) { /* ignored */ }
public void cancel(VendingMachine vm) { /* no-op */ }
}
public final class HasMoneyState implements VendingState {
public void insertCoin(VendingMachine vm, Coin coin) {
vm.session().add(coin); // self-loop, accumulate
}
public void selectProduct(VendingMachine vm, String slot) {
if (!vm.inventory().hasStock(slot)) { vm.display().show("OUT OF STOCK"); return; }
if (vm.session().deposit() < vm.inventory().priceOf(slot)) {
vm.display().show("INSERT MORE COINS");
return;
}
vm.inventory().reserve(slot);
vm.setState(new DispensingState(slot));
}
public void cancel(VendingMachine vm) {
vm.coinReturn().eject(vm.session().drain());
vm.setState(new IdleState());
}
public void dispense(VendingMachine vm) { /* ignored */ }
}
public final class DispensingState implements VendingState {
private final String slot;
public DispensingState(String slot) { this.slot = slot; }
public void dispense(VendingMachine vm) {
try {
vm.inventory().dispense(slot);
int change = vm.session().deposit() - vm.inventory().priceOf(slot);
vm.coinReturn().eject(vm.register().makeChange(change));
vm.setState(new IdleState());
} catch (HardwareException e) {
vm.coinReturn().eject(vm.session().drain()); // full refund
vm.inventory().lockSlot(slot); // ops must clear
vm.setState(new IdleState());
}
}
public void insertCoin(VendingMachine vm, Coin c) { vm.coinReturn().eject(c); }
public void selectProduct(VendingMachine vm, String s) { /* ignored */ }
public void cancel(VendingMachine vm) { /* too late */ }
}
Two non-obvious details: the Session (not the state object) carries the running deposit so cancelling collapses to a single object swap, and the dispense failure path issues a full refund and locks the slot rather than just refunding change. That separation is what lets a low-paid technician clear the slot later without touching cash logic.
Design patterns used#
- State is the dominant pattern. Each phase of the vend lifecycle is its own
VendingStateimplementation, and theVendingMachinecontext just forwards events to the current state. AddingMaintenanceState,OutOfServiceState, orRefrigerationFaultStatedoes not modify any existing class. - Strategy drives the change-making algorithm.
ChangeStrategy.split(amount, coinInventory)is typically a greedy denomination breakdown (largest coins first that the cassette still has), but an interviewer may ask for a dynamic-programming variant that minimises coin count when the greedy answer fails; swapping the strategy is one constructor argument. - Observer handles inventory and hardware notifications. The
Inventoryis the subject;LowStockAlerter,RestockEmailer, andMaintenanceDashboardare observers that subscribe toonSlotBelowThreshold(slot)events. The same channel carriesonCoinHopperLow(denom)from theCashRegister. - Singleton is the right fit for the
CoinDispenserandInventorycontrollers because the JVM owns exactly one physical hopper and one rack per machine. Making either non-singleton invites a second instance that overwrites the first hopper's counters. - Command wraps each
Transaction(a coin insertion, a product reservation, a dispense) so it can be journalled, replayed, and reversed by id; this is what makes journal-based recovery after power loss possible.
Common variants & follow-ups#
- Cashless payment: card swipe, NFC tap, or QR-scanned UPI. A
PaymentMethodinterface withCoinPayment,CardPayment, andUpiPaymentimplementations replaces the coin hopper for the deposit step. The state machine collapsesCoinInsertedinto a singlePaymentAuthorisedtransition because the amount is exact by construction, which also meansChangeReturnedis skipped on the happy path. - Multi-currency vending (airport, border kiosks) adds a
Currencyfield on everyCoinand aRateProviderstrategy that converts the deposit to the machine's pricing currency before theselectProductcheck. The FX margin is recorded as a separate journal entry for accounting. - Refrigerated vending for cold drinks, dairy, or pharmacy products adds a parallel temperature state machine:
Cooling -> WithinRange -> WarmingFault. If the temperature crosses the safe threshold the product slot is locked and the user seesTEMP FAULTinstead of an in-stock count; vending of perishables is blocked until maintenance resets the alarm. - Refill workflow is a privileged
MaintenanceStatereached by a key switch or a service-mode card. In maintenance modeinsertCoinis rerouted toCashRegister.reload(denom, count)andselectProductbecomesInventory.restock(slot, items). Audit logs are written for every restock. - Refund button: a physical or on-screen button that always works in
CoinInsertedandInsufficientFunds, and works inProductSelectedonly before the motor has fired. The behaviour is identical tocancel()in the state diagram but exposed as a discoverable UI.
Edge cases#
- Coin jam in the validator: the validator raises
CoinJamExceptionmid-insertion. The state stays in its current phase, the validator's optical path is paused, and the display showsRETURN COIN -> CALL OPS. The journal records the partial deposit so a technician can refund the user manually. - No exact change available: before transitioning to
DispensingState, the machine asksCashRegister.canMakeChange(deposit - price). If the answer is false, theselectProductcall short-circuits withEXACT CHANGE ONLYand the user can either insert smaller coins, choose a different product priced to the deposit, or cancel for a full refund. - Product jam after coin accepted: the motor stalls, the optical drop sensor never fires, and the dispense path catches
HardwareException. The refund is the entire deposit (not just change) because the user got nothing, and the slot is locked viaInventory.lockSlot(slot)so the next user is steered elsewhere until a technician clears the auger. - Power outage mid-dispense: the dispenser writes a journal entry before it energises the motor. On reboot the controller replays the journal, queries the drop sensor for its post-fault state, and either commits the sale (sensor confirms drop) or reverses the deposit (sensor empty). The reversal is idempotent on
transaction.id, so replaying twice on a flaky boot is safe. - Simultaneous selection of out-of-stock vs in-stock product: on touchscreen machines the user can tap two slots in the same animation frame. The dispatcher serialises events on the machine's event queue, processes the first slot, and if it dispenses successfully drops the second tap. If the first slot is empty the second tap is processed normally; the state machine never sees overlapping
selectProductcalls.
Glossary & fundamentals#
| Concept | What it is | Fundamentals |
|---|---|---|
| State machine | VM states | mvcc-isolation-levels |
| Idempotency | retry-safe pay | idempotency-retries |
Quick reference#
Functional#
- Browse items, select.
- Accept cash or card / UPI.
- Make change.
- Dispense item.
- Refund on cancel.
Non-functional#
- Reliable state transitions; never charge without dispense.
- Refill / maintenance mode.
Patterns#
- State, Strategy (payment), Singleton (machine), Observer (low stock).
Trade-offs#
- Multiple payment methods complicate state; isolate per PaymentMethod.
- No change available → reject coin or reduce options.
Refs#
- Common LLD references (Grokking OOD).
FAQ#
Why use the State pattern in a vending machine?#
Behaviour of select, pay, and dispense changes per state (Idle, Selecting, Dispensing, Refunding). The State pattern moves transition logic into state classes instead of nested ifs.
How is change calculation handled?#
Greedy algorithm over coin denominations, biggest first, while tracking each denomination's stock. If exact change is impossible, the machine refuses the sale and returns the inserted coins.
How is inventory tracked?#
Each Slot has a product and a count. Selection checks count greater than zero; on dispense the count decrements atomically. Restocking is an admin-mode operation gated by auth.
What states does the machine cycle through?#
Idle when waiting for input, Selecting after a product is chosen, AwaitingPayment, Dispensing during vend, Refunding on cancel, and Maintenance for restock or repair.
How do you support multiple payment methods?#
An abstract PaymentMethod with CoinPayment, CardPayment, and NfcPayment subclasses. The state machine calls method.charge(amount) and listens for an approval event before dispensing.
Related Topics#
- Parking Lot: similar resource-allocation LLD
- ATM Machine: another State-pattern LLD
- State Machines: the core pattern for vending workflows