Learning pathsPricingChecking sign-in…
← LLD problem library

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.

Local lifecycle and external execution

Hardware executes outside the monitor. UNKNOWN stays DISPENSING; cancel is allowed only from COLLECTING.

  1. IDLE: no session
  2. ↓ Select product

    COLLECTING: customer escrow
  3. ↓ Confirm validated change plan

    DISPENSING: stock reserved
  4. ↓ Accept definite success or failure

    IDLE: terminal result recorded
Outcome decision

UNKNOWN is deliberately not a terminal branch. Real partial delivery needs a richer device contract.

Correlated pending attempt

Definite successful vend

SUCCESS: commit price / change

Definite no-delivery failure

FAILED: restore stock / refund

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.

product: relationship map

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.

product relationshipsproduct 1 to slot 0..*: stocks. slot.product_id → product.id. product 1 to session 0..*: is selected by. session.product_id → product.id.stocks1 parent · 0..* childrenslotid [PK]product_id [FK]availableis selected by1 parent · 0..* childrensessionid [PK]product_id [FK]stateproductid [PK]price
slot: relationship map

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.

slot relationshipsslot 1 to vend_attempt 0..*: supplies vends. vend_attempt.slot_id → slot.id.supplies vends1 parent · 0..* childrenvend_attemptid [PK]session_id [FK]slot_id [FK]slotid [PK]product_id [FK]available
session: relationship map

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.

session relationshipssession 1 to vend_attempt 0..*: owns attempts. vend_attempt.session_id → session.id.owns attempts1 parent · 0..* childrenvend_attemptid [PK]session_id [FK]slot_id [FK]sessionid [PK]product_id [FK]state

Entity definitions and field contracts

product

Stable item and bounded price.

FieldType / keyMeaning and reference
idtext · PKProduct identity
priceinteger · VALUEMinor-unit selling price

Code mapping: Product record.

  • Positive price within declared machine bound.

slot

Physical stock position.

FieldType / keyMeaning and reference
idtext · PKSlot identity
product_idtext · FKStocked product · product.id
availableinteger · VALUEUnreserved 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.

FieldType / keyMeaning and reference
iduuid · PKCustomer interaction
product_idtext · FKSelected product · product.id
stateenum · VALUECOLLECTING or DISPENSING
escrowjson · VALUEDenomination counts

Code mapping: Machine state and escrow.

  • At most one active session per machine; validate supported denomination/counts.

vend_attempt

One logical hardware action.

FieldType / keyMeaning and reference
iduuid · PKStable command identity
session_iduuid · FKEscrow owner · session.id
slot_idtext · FKReserved physical stock · slot.id
outcomeenum · VALUESUCCESS 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 → childMultiplicityFK / why
product → slot

stocks

1 parent per child; 0..* children per parentslot.product_id → product.id

One product may occupy many physical slots.

product → session

is selected by

1 parent per child; 0..* children per parentsession.product_id → product.id

Session selection snapshots the product/price interpretation.

session → vend_attempt

owns attempts

1 parent per child; 0..* children per parentvend_attempt.session_id → session.id

The reference permits one pending action, with historical attempt identity retained.

slot → vend_attempt

supplies vends

1 parent per child; 0..* children per parentvend_attempt.slot_id → slot.id

Historical attempts share a slot; available/reserved accounting prevents oversell.

Transaction boundaries

  1. confirm: validate funds and bounded exact change, reserve stock and persist command identity together.
  2. complete SUCCESS: accept terminal evidence, transfer escrow and record change exactly once; definite failure releases only its reservation.

Indexes and uniqueness

  1. Unique unresolved session per machine and unresolved attempt per session.
  2. Primary attempt ID supports outcome deduplication; index slot/product for replenishment queries.

Schema traps and failure cases

  1. UNKNOWN must retain stock/escrow ownership; a timeout cannot safely refund and sell the same item again.
  2. Partial product/change hardware outcomes need additional evidence states beyond this reference.

Trace the records through one operation

  1. Customer selects product; coins enter session escrow.
  2. Confirm allocates attempt A and reserves one unit.
  3. 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.

Solution.java Java 21
import java.util.*;
/** Single-session vending model. The caller executes hardware commands outside this monitor. */
public final class Solution {
    private Solution() {}
    public enum State { IDLE, COLLECTING, DISPENSING }
    public enum Outcome { SUCCESS, FAILED, UNKNOWN }
    public record Product(String id, int price) {
        public Product { if (id == null || id.isBlank() || price < 1 || price > 10000) throw new IllegalArgumentException("product"); }
    }
    public record Command(String attemptId, String productId, Map<Integer,Integer> change) {
        public Command { change = Map.copyOf(change); }
    }
    public record Result(Outcome outcome, Map<Integer,Integer> returnedCoins) {
        public Result { returnedCoins = Map.copyOf(returnedCoins); }
    }
    public static final class Machine {
        private final Map<String,Product> products;
        private final Map<String,Integer> stock;
        private final Set<Integer> denominations;
        private Map<Integer,Integer> cash;
        private final Map<Integer,Integer> escrow = new TreeMap<>();
        private final Map<String,Result> results = new HashMap<>();
        private State state = State.IDLE;
        private Product selected;
        private Command pending;
        private Map<Integer,Integer> reservedCash;
        public Machine(List<Product> products, Map<String,Integer> stock, Map<Integer,Integer> cash) {
            var p = new HashMap<String,Product>();
            for (Product product : products) if (p.put(product.id(), product) != null) throw new IllegalArgumentException("duplicate product");
            this.products = Map.copyOf(p); this.stock = new HashMap<>(stock);
            if (!stock.keySet().equals(p.keySet()) || stock.values().stream().anyMatch(n -> n < 0)) throw new IllegalArgumentException("stock");
            if (cash.isEmpty() || cash.size() > 20 || cash.entrySet().stream().anyMatch(e -> e.getKey() < 1 || e.getKey() > 10000 || e.getValue() < 0 || e.getValue() > 1000)) throw new IllegalArgumentException("cash");
            if (value(cash) > 1_000_000) throw new IllegalArgumentException("cash capacity");
            this.cash = new TreeMap<>(cash); denominations = Set.copyOf(cash.keySet());
        }
        public synchronized void select(String id) {
            if (state != State.IDLE) throw new IllegalStateException("session active");
            Product p = products.get(id);
            if (p == null) throw new IllegalArgumentException("unknown product");
            if (stock.get(id) == 0) throw new IllegalStateException("sold out");
            selected = p; state = State.COLLECTING;
        }
        public synchronized void insert(int coin) {
            if (state != State.COLLECTING) throw new IllegalStateException("not collecting");
            if (!denominations.contains(coin) || credit() + coin > 10000) throw new IllegalArgumentException("unsupported coin or credit limit");
            escrow.merge(coin,1,Integer::sum);
        }
        public synchronized Command confirm(String attemptId) {
            if (attemptId == null || attemptId.isBlank()) throw new IllegalArgumentException("attempt");
            if (pending != null && pending.attemptId().equals(attemptId)) return pending;
            if (results.containsKey(attemptId)) throw new IllegalArgumentException("completed attempt ID; query result");
            if (state != State.COLLECTING || credit() < selected.price()) throw new IllegalStateException("not funded");
            var available = new TreeMap<>(cash); escrow.forEach((coin,n) -> available.merge(coin,n,Integer::sum));
            if (value(available) > 1_000_000) throw new IllegalStateException("cash capacity reached");
            Map<Integer,Integer> change = change(credit()-selected.price(), available);
            if (change == null) throw new IllegalStateException("exact change unavailable");
            // No mutation before a complete change plan exists.
            var after = new TreeMap<>(available); change.forEach((coin,n) -> after.put(coin,after.get(coin)-n));
            pending = new Command(attemptId,selected.id(),change); reservedCash = after;
            stock.put(selected.id(),stock.get(selected.id())-1); state = State.DISPENSING;
            return pending;
        }
        public synchronized Result complete(String attemptId, Outcome outcome) {
            Objects.requireNonNull(outcome);
            Result prior = results.get(attemptId);
            if (prior != null) {
                if (outcome != prior.outcome() && outcome != Outcome.UNKNOWN) throw new IllegalStateException("contradictory result");
                return prior;
            }
            if (state != State.DISPENSING || !pending.attemptId().equals(attemptId)) throw new IllegalArgumentException("unknown attempt");
            if (outcome == Outcome.UNKNOWN) return new Result(outcome,Map.of()); // Keep reservation and escrow.
            Result result;
            if (outcome == Outcome.SUCCESS) { cash = reservedCash; result = new Result(outcome,pending.change()); }
            else { stock.put(selected.id(),stock.get(selected.id())+1); result = new Result(outcome,escrow); }
            results.put(attemptId,result); reset(); return result;
        }
        public synchronized Map<Integer,Integer> cancel() {
            if (state != State.COLLECTING) throw new IllegalStateException("cannot cancel");
            Map<Integer,Integer> refund = Map.copyOf(escrow); reset(); return refund;
        }
        public synchronized Result result(String attemptId) { return results.get(attemptId); }
        public synchronized State state() { return state; }
        public synchronized int stock(String id) { if (!stock.containsKey(id)) throw new IllegalArgumentException("product"); return stock.get(id); }
        public synchronized int credit() { return value(escrow); }
        public synchronized int cashValue() { return value(cash); }
        private void reset() { escrow.clear(); selected=null;pending=null;reservedCash=null;state=State.IDLE; }
    }
    public static int value(Map<Integer,Integer> coins) {
        int sum=0; for(var e:coins.entrySet())sum=Math.addExact(sum,Math.multiplyExact(e.getKey(),e.getValue()));return sum;
    }
    /** Bounded subset-sum DP, not greedy: descending sums isolate each physical coin. */
    public static Map<Integer,Integer> change(int amount, Map<Integer,Integer> inventory) {
        if (amount < 0 || amount > 10000) throw new IllegalArgumentException("amount");
        var ways = new ArrayList<Map<Integer,Integer>>(Collections.nCopies(amount+1,null));ways.set(0,Map.of());
        for(var e:new TreeMap<>(inventory).entrySet()) {
            int coin=e.getKey(),count=e.getValue();
            if(coin<=0||count<0)throw new IllegalArgumentException("inventory");
            for(int copy=0;copy<Math.min(count,amount/coin);copy++) {
                for(int sum=amount;sum>=coin;sum--)if(ways.get(sum)==null&&ways.get(sum-coin)!=null) {
                    var combination=new TreeMap<>(ways.get(sum-coin));combination.merge(coin,1,Integer::sum);ways.set(sum,Map.copyOf(combination));
                }
            }
        }
        return ways.get(amount);
    }
}

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
ActionState after actionInvariant
Insert 10 for tea costing 4COLLECTING; escrow 10; cash unchangedTender is not revenue
Confirm ADISPENSING; stock reserved; change 6No second session can consume the reservation
Device result unknownStill DISPENSING; credit 10 retainedUnknown does not authorize refund or retry
Reconcile SUCCESSIDLE; cash increased by 4; change 6 returned10 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
ActionState after actionInvariant
Reserve tea for jam attemptStock falls from 1 to 0One reserved item
Receive definite FAILEDStock restored to 1; original coin 10 returnedCash 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

Choose a deep dive

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.

Solution.java
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.

Open focused practice →

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.