LLD-002 · LOW LEVEL DESIGN
Vending Machine
Model state, money and a physical side effect without confusing a timeout with failure.
Local Java 21 reference verified: 729 checks. Learner drafts are not executed. Author self-review; independent review pending.
Existing introductory lesson · Version 1
Understand the problem
Design one vending machine that accepts supported coins, sells one product per session, returns exact change and handles a confirmed jam or an unknown dispense result. Derive legal transitions before choosing patterns. Explain what can be rolled back in memory and what cannot be assumed about hardware.
Before you start
- Encapsulation and immutable values
- Finite-state machines
- Exact integer money
- Compound atomic operations
Scope and explicit decisions
- One active customer session per machine. Products, prices and initial stock are fixed for this reference; there is no restock, maintenance mode, card network or persistence implementation.
- Money is integer minor units in one declared currency. Product prices and session credit are bounded to 10,000. Up to 20 supported coin denominations, each 1..10,000; initial count is 0..1000. The cash box has a 1,000,000 minor-unit limit.
- Selecting an in-stock product starts COLLECTING. Coins remain in escrow until success; cancellation before confirmation returns exactly those coins. Unsupported coins and credit overflow are rejected before mutation.
- confirm produces a command but never calls hardware. The caller executes that command outside the machine monitor. Retrying confirm while the same attempt is pending returns the same command. The adapter must ensure command retries do not trigger duplicate physical delivery.
- The simulator treats SUCCESS as the entire vend-and-change action completing, FAILED as definitely no delivery/no cash transfer, and UNKNOWN as unresolved. Real devices with partial product/change outcomes need additional states and reconciliation.
- Only a successful result transfers escrow into the cash box and gives change. A definite failure restores reserved product stock and returns original tender; unknown retains reservation and blocks another session.
- Completed attempt IDs cannot start new operations. Matching duplicate outcomes return the recorded result; a contradictory later terminal outcome is rejected. Result history is retained for the process lifetime and is not durable.
- Change may use escrow plus existing cash, assuming hardware supports that cash-flow model. The algorithm finds any valid bounded combination, not necessarily the minimum number of coins. A device that cannot reuse escrow requires a different inventory contract.
Worked examples
Input: Tea costs 4. Insert coin 10. Cash includes two coins of 3.
Outcome: Reserve tea, plan change 6, then SUCCESS adds 4 to cash and returns 6.
Tender is not revenue until the physical outcome is accepted.
Input: The same funded attempt receives UNKNOWN, then definite FAILED.
Outcome: UNKNOWN leaves escrow/reservation intact; FAILED restores stock and returns the original 10 coin.
A nonterminal uncertainty and a definite no-delivery result have different transition rules.
Try before revealing
Hint 1
List legal actions in IDLE, COLLECTING and DISPENSING.
Hint 2
Which money belongs to the customer before the product is delivered?
Hint 3
Do not reserve until change is feasible; do not resolve uncertainty as failure.
Requirements and invariants
R1 — Reserve one product only after funded session and exact change are validated. confirm protects I1.
R2 — Keep tender separate until a definite physical outcome. complete protects I2.
R3 — A duplicate terminal result cannot dispense, refund or restore stock twice. complete protects I3.
I1: Available stock never falls below zero; one DISPENSING session owns exactly one reserved item.
I2: Before success, escrow is customer-owned and cash is unchanged. On success, tender = price + change.
I3: One attempt has at most one accepted terminal outcome; UNKNOWN is not terminal.
Derive the design
A transition table before a pattern hierarchy
Start with IDLE, COLLECTING and DISPENSING plus a switch/guard for each command. That is a correct, inspectable state machine for this small behavior set. The reference uses an enum and guarded methods. Introducing IdleState/PaidState/DispensingState classes is not automatically better: it is justified only if each state accumulates substantial behavior or independent variation.
Alternative: A State-pattern implementation can delegate actions to state objects, but the aggregate must still own money and stock. Letting each state mutate shared maps independently spreads the invariant across classes. Keep one transition table as the review oracle regardless of representation.
Clarify the physical contract
Ask whether cash, product and change can fail independently. The reference deliberately models one all-or-nothing device outcome; write this assumption down before claiming rollback correctness. Partial delivery belongs in a richer recovery model.
Validate the whole plan before claiming resources
Check the session, credit, available cash capacity and exact change first. A failed change search must leave stock and escrow intact. Only then reserve stock and expose a command.
Avoid locks around hardware
The synchronized domain method returns a command. Hardware invocation occurs outside the monitor. Callback processing reacquires ownership and checks the attempt ID. This separates local atomicity from external latency.
Give every branch an accounting equation
Success increases revenue by price, returns change and consumes one item. Definite failure restores the item and returns tender without changing cash. Unknown does neither until evidence arrives.
Responsibilities and ownership
Machine
Owns: Session state, stock, escrow, pending command and outcome ledger
Methods: select, insert, confirm, complete, cancel
Serializes local decisions; performs no hardware I/O.
Product
Owns: Stable product identity and bounded price
Methods: Validated immutable record
Does not accept money or invoke a dispenser.
Command / Result
Owns: Immutable attempt identity and change/refund values
Methods: Defensive Map.copyOf constructors
An instruction/result is not itself proof of device execution.
change algorithm
Owns: A pure search over a copied inventory
Methods: change(amount, inventory)
Returns a complete valid plan or null before domain mutation.
Hardware executes outside the monitor. UNKNOWN stays DISPENSING; cancel is allowed only from COLLECTING.
- IDLE: no session
↓ Select product
COLLECTING: customer escrow↓ Confirm validated change plan
DISPENSING: stock reserved↓ Accept definite success or failure
IDLE: terminal result recorded
UNKNOWN is deliberately not a terminal branch. Real partial delivery needs a richer device contract.
Definite successful vend
Definite no-delivery failure
Data model, schema and relationships
Separate catalog identity, physical inventory, active customer session and an immutable dispense attempt. Coin escrow belongs to the active session until trusted hardware evidence resolves it.
Implementation boundary: Machine owns these values in memory. A relational journal would persist attempts/outcomes; it still cannot transact atomically with a physical dispenser.
These are proposed logical schemas with field contracts, not executable database migrations. PK identifies a row, FK references an existing parent, UK is a uniqueness rule. A composite PK identifies the combination. Nullable references are marked with ?. A foreign key protects identity; cross-row limits, timing and state transitions also require the transaction rules below.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Entity definitions and field contracts
product
Stable item and bounded price.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | text · PK | Product identity |
price | integer · VALUE | Minor-unit selling price |
Code mapping: Product record.
- Positive price within declared machine bound.
slot
Physical stock position.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | text · PK | Slot identity |
product_id | text · FK | Stocked product · product.id |
available | integer · VALUE | Unreserved quantity |
Code mapping: Product stock map; slot identity is persistence extension.
- available>=0; slot cannot change product during an unresolved vend.
session
One active customer escrow owner.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Customer interaction |
product_id | text · FK | Selected product · product.id |
state | enum · VALUE | COLLECTING or DISPENSING |
escrow | json · VALUE | Denomination counts |
Code mapping: Machine state and escrow.
- At most one active session per machine; validate supported denomination/counts.
vend_attempt
One logical hardware action.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Stable command identity |
session_id | uuid · FK | Escrow owner · session.id |
slot_id | text · FK | Reserved physical stock · slot.id |
outcome | enum · VALUE | SUCCESS FAILED UNKNOWN |
Code mapping: Pending command and outcome ledger.
- One unresolved attempt per active session; duplicate definite outcomes must agree.
Relationship cardinalities and their reasons
| Parent → child | Multiplicity | FK / why |
|---|---|---|
| product → slot stocks | 1 parent per child; 0..* children per parent | slot.product_id → product.idOne product may occupy many physical slots. |
| product → session is selected by | 1 parent per child; 0..* children per parent | session.product_id → product.idSession selection snapshots the product/price interpretation. |
| session → vend_attempt owns attempts | 1 parent per child; 0..* children per parent | vend_attempt.session_id → session.idThe reference permits one pending action, with historical attempt identity retained. |
| slot → vend_attempt supplies vends | 1 parent per child; 0..* children per parent | vend_attempt.slot_id → slot.idHistorical attempts share a slot; available/reserved accounting prevents oversell. |
Transaction boundaries
- confirm: validate funds and bounded exact change, reserve stock and persist command identity together.
- complete SUCCESS: accept terminal evidence, transfer escrow and record change exactly once; definite failure releases only its reservation.
Indexes and uniqueness
- Unique unresolved session per machine and unresolved attempt per session.
- Primary attempt ID supports outcome deduplication; index slot/product for replenishment queries.
Schema traps and failure cases
- UNKNOWN must retain stock/escrow ownership; a timeout cannot safely refund and sell the same item again.
- Partial product/change hardware outcomes need additional evidence states beyond this reference.
Trace the records through one operation
- Customer selects product; coins enter session escrow.
- Confirm allocates attempt A and reserves one unit.
- Repeated A returns same command; definite result commits or releases the owned reservation.
Method contracts
select(productId), insert(coin)
Select requires IDLE and available stock. Insert requires COLLECTING, supported denomination and credit <=10,000. Rejections preserve the prior session.
confirm(attemptId) → Command
Check funds, cash capacity and bounded change first; reserve stock and enter DISPENSING atomically. Same pending attempt ID returns the command without reserving again.
complete(attemptId, outcome) → Result
Trusted device adapter only. UNKNOWN keeps state; SUCCESS commits planned cash and change; FAILED restores stock/refunds escrow. Contradictory terminal outcomes fail.
cancel() → returned coin map
COLLECTING only. Return original coins and reset; cannot cancel while a physical outcome is unresolved.
result(attemptId) → Result or null
Query completed results instead of reusing an old ID for a new vend. Retained history has no restart durability.
What, when, why and how
Each concept is attached to the requirement it protects, its implementation, an alternative and a test.
Finite-state machine
- what
- A contract describing which commands are legal in each lifecycle stage.
- when
- When actions such as insert, cancel and confirm have state-dependent meaning.
- why
- Without guards, a customer could cancel after physical delivery started or insert money into a finished session.
- how
- select moves IDLE→COLLECTING; confirm validates before COLLECTING→DISPENSING; complete resolves to IDLE only on a terminal result. cancel accepts COLLECTING only.
Code: State, select, confirm, complete, cancel · Invariants: I1, I3
Alternatives, cost and failure example
Alternative: State classes are an alternative representation, not a stronger correctness guarantee.
Cost: A small enum is compact; many nested conditionals indicate a need to revisit organization.
Counterexample: cancel refunds after confirm while a delayed dispenser also delivers a product.
Test: Assert cancellation during DISPENSING fails; UNKNOWN must leave DISPENSING unchanged.
Escrow and reservation
- what
- Separate customer tender and temporarily held stock from committed revenue.
- when
- Before a physical action may fail or take time.
- why
- Crediting cash at insert time makes refund and failure accounting difficult; decrementing stock only after delivery allows double commitment.
- how
- insert stores a coin count in escrow. confirm reserves one stock unit and prepares cash-after-success. complete commits or releases according to the definite result.
Code: insert, confirm, complete · Invariants: I1, I2
Alternatives, cost and failure example
Alternative: A database reservation is the durable analogue, but no local transaction rolls back a physical product drop.
Cost: One session simplifies ownership but prevents serving another customer while outcome is unknown.
Counterexample: A jam marks cash as revenue despite returning the customer’s original tender.
Test: Test cash unchanged during pending/unknown, restored stock on FAILED, and revenue increase equal to price on SUCCESS.
Bounded change-making
- what
- Choose coins subject to finite inventory counts.
- when
- When exact change must be returned with arbitrary supported denominations.
- why
- Greedy largest-first can fail even when a valid combination exists.
- how
- Use subset-sum reachability indexed by amount. For each physical coin, traverse sums downward so that coin cannot be reused in the same pass; save a valid coin map for newly reachable sums.
Code: change, value · Invariants: I2
Alternatives, cost and failure example
Alternative: For a proven canonical denomination system with adequate stock greedy may suffice. Finite stock can break assumptions; test the exact contract.
Cost: If A is change amount, C considered coins and D denominations, O(C×A) checks plus copying up to O(D) entries when new amounts become reachable; O(A×D) retained maps in the worst case.
Counterexample: For amount 6 with one 4 and two 3s, greedy takes 4 then fails; 3+3 works.
Test: Compare all amounts 0..12 over bounded {1,3,4} inventories with an independent exhaustive combination oracle.
Attempt identity and ambiguous outcomes
- what
- Distinguish one logical dispense from multiple messages about it.
- when
- A hardware call or acknowledgement times out.
- why
- Unknown is not proof of failure; blind retries or refunds can duplicate physical value.
- how
- confirm returns an attempt ID. UNKNOWN holds the reservation; reconciliation later supplies SUCCESS or definite FAILED. Terminal results are deduplicated and contradictory results rejected.
Code: Command, complete, result · Invariants: I3
Alternatives, cost and failure example
Alternative: A real adapter may query device state by operation ID; if hardware cannot dedupe or reconcile, route to manual service rather than promise exactly once.
Cost: Result retention grows; process loss currently loses all evidence.
Counterexample: Treat a timeout as FAILED, restore stock, then accept a delayed SUCCESS from the old attempt.
Test: Test UNKNOWN then SUCCESS, duplicate SUCCESS and conflicting FAILED, ensuring no extra stock or money mutation.
Read and run the Java reference
Reveal complete Solution.java
Java 21. Save as Solution.java; compile with javac --release 21. The repository also contains Test.java and the lld:java verification command. This reference implements the local contract above.
confirm performs preparation before mutation
The complete change combination is computed before pending, reservedCash, stock or state change. Once DISPENSING is visible, another session cannot consume the planned resources.
Descending DP indices prevent coin reuse
For one physical coin, sum descends from amount to denomination. sum-coin has not yet been modified by that coin during this pass. Repeating for each available copy respects bounded inventory.
complete first resolves duplicates
A terminal result is looked up before active-session checks, so a duplicate can return its original outcome even after the machine starts another session. A conflicting terminal result fails.
Immutable result maps protect accounting
Command and Result copy their maps. Clearing internal escrow during reset cannot erase the refund returned to the caller.
Complexity and limits
- select, insert and terminal lookup use expected O(1) map access; value computation scans D supported denominations.
- Change search examines up to C=sum(min(count, A/denomination)) physical coins across A amounts; copying a newly discovered combination costs up to O(D). Space is O(A×D). Bounds A<=10,000 and D<=20 make the educational search finite, not necessarily a latency target.
- Outcome history grows O(completed attempts); product/cash state is bounded by configured products/denominations. A production retention policy needs durable reconciliation before deletion.
- A single monitor serializes state changes. Change computation occurs inside it; if measured latency is high, compute on a versioned snapshot and validate before commit rather than drop synchronization.
Walk through the actual reference
These walkthrough fixtures are emitted only after the Java harness verifies their operations and expected state. Concurrency tests additionally exercise competing callers.
Tender, reservation and physical outcome
Step 1 of 4: Insert 10 for tea costing 4
COLLECTING; escrow 10; cash unchanged
Invariant: Tender is not revenue
Read the complete trace table
| Action | State after action | Invariant |
|---|---|---|
| Insert 10 for tea costing 4 | COLLECTING; escrow 10; cash unchanged | Tender is not revenue |
| Confirm A | DISPENSING; stock reserved; change 6 | No second session can consume the reservation |
| Device result unknown | Still DISPENSING; credit 10 retained | Unknown does not authorize refund or retry |
| Reconcile SUCCESS | IDLE; cash increased by 4; change 6 returned | 10 tender = 4 revenue + 6 change |
Definite jam and refund
Step 1 of 2: Reserve tea for jam attempt
Stock falls from 1 to 0
Invariant: One reserved item
Read the complete trace table
| Action | State after action | Invariant |
|---|---|---|
| Reserve tea for jam attempt | Stock falls from 1 to 0 | One reserved item |
| Receive definite FAILED | Stock restored to 1; original coin 10 returned | Cash unchanged; no product delivered under FAILED contract |
Mistakes and counterexamples
Use greedy change for all inventories
Failing case: Amount 6, inventory {4:1,3:2}.
Repair: Use bounded reachability or justify the exact greedy assumptions.
Refund on any timeout
Failing case: The product dropped but acknowledgement was lost.
Repair: Keep UNKNOWN unresolved and reconcile the physical operation.
Clear escrow before constructing refund
Failing case: A returned view points to the cleared map.
Repair: Take a defensive copy before resetting session state.
Execute the dispenser inside synchronized confirm
Failing case: A slow device holds the monitor across seconds of network I/O.
Repair: Return a command and accept a later correlated outcome.
Engineer → Senior → Staff
Build a complete single-session state model.
Executed: selection, coin validation, finite-stock change, cancellation and definite success/failure.
Change exercise
Add a failing example for greedy change and explain every state transition.
Worked answer guidance
Use amount 6 with {4:1,3:2}. Draw guards before adding State classes; demonstrate invalid actions leave credit and stock unchanged.
Interview follow-ups
Why not one subclass per state immediately?
The enum/table is sufficient for three states. State objects help when behavior grows, but must not split aggregate ownership.
Does this DP minimize returned coins?
No. It finds a valid combination. A minimum-coin requirement needs an objective and predecessor choice, with separate tests.
Your design attempt
Each language has a separate draft across all three levels. Saving is device-only and does not sync to your account. Anyone using this browser profile can load it. Loading is always explicit.
Java 21 reference code is provided for local study. Use Run program in the editor to execute your code and test driver. Program runs show output; they do not grade your design.
Run program · Java 21
No unsaved changes · 208/20000
Self-review
Checks apply to this visit. Self-review is not a passed submission or verified mastery.
Provenance and scope of evidence
Original teaching material. The local contract is implemented by Solution.java and tested by Test.java on Java 21. Distributed persistence, real provider/device adapters and deployment are not claimed.
Reference verification is not independent editorial review, production deployment verification or a claim that distributed adapters are implemented.