🧠 Vending Machine LLD — Thought Process Guide¶
Goal: Learn how to think when designing a Low-Level Design.
📊 Class Diagram¶
classDiagram
direction TB
class VendingMachine {
-inventory: Inventory
-cash: CashBox
-dispenser: Dispenser
-gateway: PaymentGateway
-state: MachineState
-selected: Optional~Slot~
-escrow: Counter
-lock: RLock
+state: str
+balance_cents: int
+select(code: str) Product
+insert(piece: Denomination) Optional~Receipt~
+pay_by_card(token: str) Receipt
+cancel() Dict~Denomination, int~
+purchase_with_card(code: str, token: str) Receipt
+add_slot(code: str, product: Product, quantity: int, capacity: int) None
+restock(code: str, qty: int) int
+load_change(pieces: Mapping) None
+collect_cash(keep_float: Mapping) Dict~Denomination, int~
+enter_maintenance() Dict~Denomination, int~
+exit_maintenance() None
+stock() Dict~str, int~
+cash_total_cents() int
}
class MachineState {
<<abstract>>
+name: str
+m: VendingMachine
+select(code: str) Product
+insert(piece: Denomination) Optional~Receipt~
+pay_by_card(token: str) Receipt
+cancel() Dict~Denomination, int~
}
class IdleState {
+select(code: str) Product
}
class AwaitingPaymentState {
+insert(piece: Denomination) Optional~Receipt~
+pay_by_card(token: str) Receipt
+cancel() Dict~Denomination, int~
}
class OutOfServiceState
class Inventory {
-slots: Dict~str, Slot~
+add_slot(code: str, product: Product, quantity: int, capacity: int) None
+slot(code: str) Slot
+restock(code: str, qty: int) int
+snapshot() Dict~str, int~
}
class Slot {
<<dataclass>>
+code: str
+product: Product
+quantity: int
+capacity: int
}
class Product {
<<dataclass>>
+sku: str
+name: str
+price_cents: int
}
class CashBox {
-counts: Counter
+deposit(pieces: Mapping) None
+withdraw(pieces: Mapping) None
+counts() Dict~Denomination, int~
+total() int
}
class Dispenser {
<<abstract>>
+dispense(slot_code: str)* bool
}
class ReliableDispenser {
+dispense(slot_code: str) bool
}
class PaymentGateway {
<<abstract>>
+authorize(token: str, amount_cents: int)* str
+capture(auth_id: str)* None
+void(auth_id: str)* None
}
class FakeGateway {
+holds: Dict~str, int~
+captured: Dict~str, int~
+voided: Dict~str, int~
}
class Receipt {
<<dataclass>>
+slot_code: str
+product: Product
+method: PaymentMethod
+paid_cents: int
+change: Dict~Denomination, int~
+change_cents: int
}
class Denomination {
<<enumeration>>
NICKEL
DIME
QUARTER
DOLLAR_COIN
ONE_NOTE
FIVE_NOTE
+cents: int
+is_coin: bool
}
class PaymentMethod {
<<enumeration>>
CASH
CARD
}
VendingMachine "1" *-- "1" Inventory
VendingMachine "1" *-- "1" CashBox
VendingMachine "1" *-- "3" MachineState : idle, awaiting, out_of_service
VendingMachine o-- Dispenser : port
VendingMachine o-- PaymentGateway : port
VendingMachine ..> Receipt : returns
MachineState --> VendingMachine : m
MachineState <|-- IdleState
MachineState <|-- AwaitingPaymentState
MachineState <|-- OutOfServiceState
Inventory "1" *-- "*" Slot
Slot --> Product
Dispenser <|-- ReliableDispenser
PaymentGateway <|-- FakeGateway
Receipt --> Product
Receipt --> PaymentMethod
CashBox ..> Denomination : counts per piece
⏱️ How to Run This in a 45–60 min Interview¶
| Time | Step | What to say out loud |
|---|---|---|
| 0–5 | Clarify | "Cash only, or card too? Which denominations, and which can it pay out? Select first or pay first? What happens if it can't make change? One item per transaction? Do I need maintenance/restock?" |
| 5–12 | Entities + interfaces | "Money is integer cents. States: idle, awaiting payment, out of service. Two ports to the outside world — Dispenser and PaymentGateway — because both can fail and I want to fake them in tests." |
| 12–35 | Core code | Inventory, make_change, the state classes, then VendingMachine. Write the cash path end to end first: select → insert → change check → dispense → commit. |
| 35–45 | Failure + concurrency | "Jam: refund escrow / void the hold. Can't make change: reject the piece. One lock per machine because restock and remote purchases run alongside the keypad." |
| 45–60 | Extension | Card payments (authorize/capture/void), timeouts, multi-item cart, pricing policy. |
Clarifying questions worth asking (defaults to propose):
- Payment methods? → cash and card.
- Order: select then pay, or pay then select? → select first (price is known, change check is possible).
- Can't make change? → reject the coin/note that overpays; light "exact change only".
- Which pieces are paid out? → coins only; notes are accepted and kept.
- Partial dispense / jam? → full refund, stock unchanged.
- Concurrency? → keypad is single-user, but operator and remote purchases are concurrent.
Phase 1: Identify the Nouns¶
"A vending machine displays products. User selects a product, inserts money, and receives it with change."
| Noun | Decision | Why |
|---|---|---|
| Denomination | Enum (cents, is_coin) |
Fixed set; tuple values so a $1 coin and $1 note aren't Enum aliases |
| Product | Frozen dataclass | Immutable catalogue entry, price in cents |
| Slot | Dataclass | Where stock lives: code, product, quantity, capacity |
| Inventory | Class | Slot lookup, restock with capacity check |
| CashBox | Class | The machine's coins/notes |
| Escrow | Counter on the machine |
Customer's pieces until the sale commits |
| Dispenser | ABC | Hardware that can fail |
| PaymentGateway | ABC | Card auth/capture/void |
| MachineState | Base class + 3 states | State pattern |
| Receipt | Frozen dataclass | Result of a sale |
Phase 2: Assign Responsibilities¶
| Action | Owner | Why |
|---|---|---|
| Is this action allowed now? | Current MachineState |
No if state == ... chains |
| Can I make change? | make_change() |
Pure function, easy to test |
| Hold customer cash | Machine _escrow |
Must be returnable as-is |
| Drop the product | Dispenser |
Hardware port; may fail |
| Charge a card | PaymentGateway |
Two-phase so we never charge for nothing |
| Commit a sale | VendingMachine._vend_cash/_vend_card |
One place moves stock and money together |
Phase 3: The State Machine¶
IDLE --select(code)--> AWAITING_PAYMENT
AWAITING_PAYMENT --insert (balance < price)--> AWAITING_PAYMENT
AWAITING_PAYMENT --insert (completes, change OK)--> [dispense] --> IDLE
AWAITING_PAYMENT --insert (completes, no change)--> reject piece, stay
AWAITING_PAYMENT --pay_by_card--> [authorize, dispense, capture] --> IDLE
AWAITING_PAYMENT --cancel | jam--> refund --> IDLE
any --enter_maintenance--> (refund if open) OUT_OF_SERVICE --exit--> IDLE
Phase 4: The Order of Operations That Keeps Money Safe¶
- Validate the selection (exists, in stock).
- Accept pieces into escrow.
- On the piece that completes payment, check change before accepting it.
- Drive the motor; if the drop sensor fails, refund/void and stop.
- Commit: escrow → cash box, change out, stock − 1 (card: capture).
Phase 5: Quick Checklist¶
✅ Integer cents, never float ✅ Escrow returned as the exact pieces on cancel ✅ Change computed with bounded DP, checked before taking the money ✅ Card captured only after a confirmed drop; voided on jam ✅ Every public method atomic under one machine lock ✅ Hardware and gateway behind ABCs, faked in tests