System Design Road

Medium LLD interview problem

Design a vending machine

“Design a vending machine” is the interview problem built for the State pattern, and most answers stop there. The interviewer wants more: money that’s never lost or double-counted, change that’s actually possible to give, a motor that might jam, and hardware events arriving from several directions at once. This walkthrough builds all of it from nothing and explains every decision.

28 min read4 diagrams15 sections

This gives away every stage. For a clean attempt, practise it first, then come back.

The short version

  • A vending machine’s behaviour depends on what it’s doing right now. The same button means different things in different situations, which is the signal for the State pattern.
  • Draw a table of states against events before any classes. Each column becomes a state class; each cell becomes a method.
  • Inserted money stays in escrow until the item has actually dropped. Only then does it become the machine’s. Cancelling or a jam returns exactly what went in.
  • Change has to come from coins really in the machine. Taking the largest coin first can fail when another combination would work, so search properly.
  • Hardware talks to the machine through small interfaces and sends events from different threads. One lock on the machine keeps events in order.
  • Card payments arrive as one new state and one new Payment class. No existing state’s logic changes.

How this walkthrough is built

A vending machine looks like a toy problem. It isn’t. It’s a small, complete system with real money, physical parts that fail, and inputs arriving in any order from people who press buttons at the wrong moment. That makes it an excellent test of how you model behaviour that depends on situation.

We’ll build it in interview order, each step using only what came before:

StepWhat you doWhat comes out of it
1. Understand the problemAsk questions, write down requirementsWhat the machine must do, and what can go wrong
2. Break it into entitiesNouns, the status-flag trap, a state-by-event tableClasses, and the table that is the design
3. Choose a directionTop-down for designing, bottom-up for codingThe order you’ll work in
4. Design each classDecide what each class knows and doesA card per class
5. Connect the classesPick the right relationshipsWho owns whom
6. Draw the diagramBuild it up in three versionsThe complete class diagram
7. Name the patternsState, a Payment strategy, hardware interfacesAnd why each is there
8. Walk the flowsA sale, a cancel, a jam, call by callEscrow, and why dispensing is a real state
9. Write the codeBottom-up, one class at a timeCode that compiles and runs
10. Make it safeEvents from several threadsOne lock, and a test that counts every paisa
11. Extend itCard payments, alerts, pricingNew classes, no edits to old logic

The one sentence to remember: when the same action means different things depending on what the machine is doing, give each “what it’s doing” its own class.

Step 1Understand the problem

Picture the real thing first

Stand in front of a snack machine at a railway station. Rows of spiral coils, each with a code like A1 or B4. A slot for coins, another for notes. A keypad. A small screen. A flap at the bottom where packets drop, and a little tray where change comes out.

Now notice how much depends on timing. Press B4 before putting any money in, and the screen shows the price. Press it after putting in enough, and a packet drops. Push a coin in while the coil is turning, and it falls straight back into the tray. Press cancel after inserting money, and you get your money back, the same coins you put in. If the packet gets stuck on the coil, you want your money back too. And once a week, someone in a uniform turns a key, opens the front, refills the coils and takes the notes out.

Every one of those situations is something the design must handle.

Ask before you design

Interviewer

Interviewer: Design a vending machine.

You

You: Sure. First, what does it sell, and how is it laid out?

Interviewer

Interviewer: Snacks and drinks in numbered slots like A1 or B4. Each slot holds one product with one price.

You

You: How do people pay?

Interviewer

Interviewer: Coins of ₹1, ₹2, ₹5 and ₹10, and notes of ₹20, ₹50 and ₹100.

You

You: Does it give change?

Interviewer

Interviewer: Yes, in coins.

You

You: What if it can’t make the change?

Interviewer

Interviewer: It shouldn’t short-change anyone. Tell them, and let them put in exact money or cancel.

You

You: Can the customer cancel?

Interviewer

Interviewer: Yes, any time before the item drops. They get back what they put in.

You

You: Can the item fail to drop?

Interviewer

Interviewer: It happens. The motor turns, and a sensor at the flap says whether something fell.

You

You: Anything for staff?

Interviewer

Interviewer: A service key. Staff restock, load coins for change, and collect notes.

You

You: Will it take cards one day?

Interviewer

Interviewer: Probably next year. Not now.

You

You: Last one. Is it one customer at a time?

Interviewer

Interviewer: Yes, but the coin slot, keypad, sensor and service key are separate pieces of hardware. They can all send signals at any moment.

QuestionWhat the answer decided
Slots with one product eachSlot holds a product and a count; Inventory holds slots by code.
Coins and notesOne Denomination enum, with a flag for notes.
Change in coins, never short-changeChange must be planned from coins really held, before anything drops.
Cancel any time before the dropInserted money is kept separate until the sale completes: escrow.
The item can fail to dropDispensing takes time and has two outcomes. It’s a real state, not a step.
Staff keyA maintenance state, and staff actions that only work in it.
Cards next yearPayment sits behind an interface now, so cards can be added later.
Separate hardware, any momentEvents arrive on different threads. The machine must handle them one at a time.

Functional requirements

  1. FR1. The machine has slots, each with a product, a price and a count.
  2. FR2. Customers insert coins and notes. The screen shows the balance.
  3. FR3. Customers choose a slot. If the balance is enough, the slot has stock and change can be made, the item drops and change is returned.
  4. FR4. If the balance is short, the screen says how much more. If the slot is empty, it says sold out.
  5. FR5. If change can’t be made from the coins inside, the sale is refused and the customer can add exact money or cancel.
  6. FR6. Cancel, any time before the drop, returns exactly the money inserted.
  7. FR7. If the item fails to drop, the customer gets their money back and that slot stops selling.
  8. FR8. With the service key, staff can restock slots, load coins and collect notes. Customers can’t buy meanwhile.

Non-functional requirements

  1. NFR1: Money is never lost or invented. At every moment, money in = money in the cash box + money handed back.
  2. NFR2: Correct when hardware events overlap. A coin pushed during a drop, a sensor signal arriving late, the service key turned mid-sale: none of these can break the rules.
  3. NFR3: Easy to extend. Card payments, new states and new rules should mean adding classes, not editing working ones.
  4. NFR4: Testable without hardware. The motor, sensor, screen and coin tray sit behind interfaces, so tests can stand in for them.

Assumptions we’ll write down

AssumptionWhy it’s reasonable
Prices and money are whole paise, stored as longNo rounding errors, ever
Change is given only in coinsMachines rarely dispense notes
Notes are accepted but never used for changeThey go into a separate locked box
One item per saleBaskets are a follow-up
The motor runs on its own; the sensor reports back laterThat’s how real dispensers work

Step 2Break it into entities

Pull out the nouns

From the story and the requirements: machine, slot, product, price, count, coin, note, balance, money inserted, cash box, change, screen, keypad, motor, sensor, return tray, customer, staff, service key.

NounOwn data?Own rules?Becomes
Machineeverything belowwhat each event means right nowVendingMachine class
Slotcode, product, count, faulty?can’t sell when empty or faultySlot class
Inventoryslots by code—Inventory class
Productname, price—Product record
Coin, notevalue, is it a note?—Denomination enum
Money inserted (balance)the actual coins and notesreturned exactly on cancelan escrow list on the machine
Cash boxcoins and notes heldcan only pay out what it has; plans changeCashBox class
Screen, motor, return tray——Display, Dispenser, ReturnTray interfaces
Sensor, keypad, service key——events the machine receives, not classes
Customer, staff——nothing: they’re outside the machine

Two rows deserve a closer look.

The balance isn’t just a number. If the balance were a number, cancelling ₹50 would mean giving back ₹50 in coins, which the machine might not have. Real machines keep the actual inserted coins and notes in a holding area called escrow, and return those exact pieces on cancel. Only when the sale completes do they drop into the cash box. Keeping a list of denominations, not a total, makes cancel always possible and FR6 easy.

Hardware becomes interfaces and events. The screen, motor and tray are things the machine tells to do something, so they’re interfaces it calls. The coin slot, keypad, sensor and service key are things that tell the machine something, so they become methods on the machine: insert, select, dropFinished, serviceKey. Hardware code calls those.

The status-flag trap

The most common first design gives the machine a status field and switches on it everywhere:

Java
enum Status { IDLE, HAS_MONEY, DISPENSING, SERVICE }

void insert(Denomination d) {
    switch (status) {
        case IDLE -> { escrow.add(d); status = Status.HAS_MONEY; }
        case HAS_MONEY -> escrow.add(d);
        case DISPENSING, SERVICE -> tray.give(List.of(d)); 1
    }
}

void select(String code) {
    switch (status) {
        case IDLE -> display.show("Insert money");
        case HAS_MONEY -> { /* the whole sale: stock, price, change, dispense */ }
        case DISPENSING -> display.show("Please wait");
        case SERVICE -> display.show("Out of service");
    }
}
// ...and the same switch again in cancel(), dropFinished(), serviceKey()
  1. 1refused

It works on day one. But follow what happens next. The rules for “has money” are spread across five methods. Adding card payments means a new status, and a new branch in every switch, in methods that already work. Miss one branch and the compiler won’t tell you; some button just does the wrong thing in one situation. And every method knows about every situation, so nothing can be tested on its own.

The way out is to turn the table sideways. Instead of each event knowing about every situation, make each situation a class that knows how to handle every event.

The state-by-event table

Before writing any classes, draw this table. It’s the single most useful thing you can put on the whiteboard for this problem: every cell is a decision, and empty cells are bugs waiting to happen.

Event ↓ / State →IdleHas moneyDispensingOut of service
insert coin/notehold it; → Has moneyhold it; show balancegive it straight backgive it straight back
select slotshow the pricesell if possible, else explain“please wait”“out of service”
cancelnothing to doreturn escrow; → Idletoo late: ignorenothing to do
drop finished(can’t happen)(can’t happen)success: complete sale; jam: refund; → Idle(can’t happen)
service key→ Out of servicereturn escrow; → Out of service“please wait”→ Idle

Read down a column and you have one state’s complete behaviour. Each column becomes a class, and each cell a method. That’s the State pattern from chapter 6, and the table is how you arrive at it honestly.

Notice the “please wait” and “give it straight back” cells. Most states refuse most events. That’s a hint for step 4: a base class with sensible refusals, so each state only writes the cells that do something.

Pull out the verbs

Use caseWho starts itWhat happens
UC1: Buy with cashCustomerinsert money, select, plan change, start the motor, sensor confirms, complete the sale
UC2: CancelCustomerreturn exactly what was inserted
UC3: Can’t sellCustomersold out, short balance, or no change: explain, stay ready
UC4: JamSensorrefund, mark the slot faulty
UC5: ServiceStaffkey in, restock, load coins, collect notes, key out

Give each job to the class that knows the most

JobInformation neededOwner
Decide what an event means right nowthe current situationthe current state object
Can this slot sell? Take one itemcount, faulty flagSlot
Find a slot by codeall slotsInventory
Can we make ₹X change? Pay it outcoins heldCashBox
What did this customer put in?the escrow listVendingMachine
Finish or undo a paymenthow it was paidPayment (cash today, card later)
Show text, turn the motor, return moneythe hardwareDisplay, Dispenser, ReturnTray

CRC cards

ClassResponsibilitiesCollaborators
VendingMachinereceive events, pass them to the current state; hold escrow and current stateMachineState, Inventory, CashBox, hardware
MachineState + 4 statesdecide what each event means in this situation; switch stateVendingMachine
Slot, Inventorystock per code; sellable or notProduct
CashBoxhold money; plan exact change; pay outDenomination
Payment, CashPaymentcomplete or refund one saleCashBox, ReturnTray

Find what will change

Will changeHintBecomes
New situations (card waiting, broken, low power)“cards next year”one class per state, behind MachineState
How a sale is paid“cards next year”Payment interface; CashPayment today
The hardware itselfdifferent models, and testsDisplay, Dispenser, ReturnTray interfaces

Step 3Top-down or bottom-up?

Top-down, starting from the hardware: the coin slot calls machine.insert(d). The machine doesn’t decide; it asks state.insert(machine, d). The has-money state needs to know whether the slot can sell (so Slot.canSell()), whether change is possible (so CashBox.planChange()), and how to start the drop (so Dispenser.startDrop()). Each piece appears because something above needed it.

Bottom-up, for coding: Denomination and Product, then Slot and Inventory, then CashBox (the trickiest logic, so get it right early), then the payment and hardware interfaces, then the states, and the machine last. By the time you write HasMoneyState, every method it calls already exists and works.

Step 4Design each class

VendingMachine: the context

Job: receive every hardware event and pass it to the current state. Hold the shared data the states work on.

KnowsDoesDoesn’t do
current stateinsert, select, cancel, dropFinished, serviceKeydecide what an event means (the state does)
inventory, cash boxstaff actions: restock, loadCoins, collectNotesplan change (the cash box does)
escrow: the money inserted this timebeginSale(slot, payment), used by states
display, dispenser, return tray

In State-pattern language, the machine is the context: the object whose behaviour changes. Its event methods are one line each: state.insert(this, d). All the decisions live in the states.

Staff actions are different: they only check “are we in service mode?” and then do the job. They don’t vary by state in any interesting way, so routing them through every state would add methods without adding meaning.

MachineState and the four states

Job: decide what each event means in one situation.

MachineState is an abstract class: a class that can’t be created on its own and exists to be extended. It provides a default for every event, the common refusals from the table: “please wait”, or hand the coin back. Each state overrides only the cells where it does something real.

StateOverridesMain rule
IdleStateinsert, select, service keyfirst coin starts a session; select only shows the price
HasMoneyStateinsert, select, cancel, service keythe sale decision lives here
DispensingStatedrop finishedonly the sensor can end this state
OutOfServiceStateinsert, select, service keyrefuses customers; key returns to Idle

DispensingState holds data: the slot being dispensed and the payment. That’s why each sale creates a new DispensingState object instead of sharing one. The other three hold nothing, so they could be shared single objects, but creating them is so cheap that it isn’t worth the complication.

Why an abstract class and not an interface with default methods? Either works. The abstract class makes it explicit that these are “base behaviours”, and it’s where the name() method (for logs and tests) is required. Saying “either works, here’s why I picked this one” is a good habit in interviews.

Slot and Inventory

Job: track stock per code, and whether a slot can sell.

Slot knowsSlot does
code, product, count, faultycanSell(): count > 0 and not faulty
takeOne(): refuses if empty
markFaulty(), restock(product, n)

Inventory is a map from code to slot. It returns Optional, so “no such code” can’t be mistaken for a slot.

CashBox: the money and the change

Job: hold coins and notes, plan exact change from coins really present, and pay out.

KnowsDoes
count of each denominationdeposit(money)
planChange(amount, customerCoins): a list of coins, or nothing
payOut(coins): refuses if a coin isn’t there
collectNotes()

planChange doesn’t change anything; it only answers “is it possible, and with which coins?”. The coins only leave the box later, in payOut, after the item has dropped. Planning and paying are separate on purpose: we must know change is possible before the motor turns, but we mustn’t hand it over until the sale is certain.

It also takes the customer’s own coins into account. If the cash box is empty and someone inserts a ₹10 coin for an ₹8 item, the machine can’t give ₹2 change. But if they insert two ₹5 coins for a ₹5 item, one of their own ₹5 coins is the change.

Payment and CashPayment

Job: finish one sale, or undo it.

Payment has two methods: complete(machine) and refund(machine). CashPayment remembers the inserted money and the planned change. Complete: move the inserted money into the cash box, pay out the change, hand it to the tray. Refund: hand back the inserted money, untouched.

Why bother, when only cash exists today? Because the dispensing state needs to finish or undo a sale without knowing how it was paid. When card payments arrive, CardPayment.complete captures the card charge and refund releases it, and DispensingState doesn’t change at all.

The hardware interfaces

Display.show(text), Dispenser.startDrop(code) and ReturnTray.give(money). Each has one method, so a test can pass a lambda: shown::add collects every message for checking. Note that startDrop only starts the motor. The result comes back later, as a separate event: machine.dropFinished(ok).

Step 5How the classes relate

RelationshipIn this designWhy that kind
Compositionmachine owns its escrow listthe escrow means nothing outside this machine
Aggregationmachine holds inventory and cash boxcreated outside and passed in; could be inspected or swapped
Associationmachine → current MachineStatechanges over time
Inheritance4 states extend MachineStateshared default behaviour
RealizationCashPayment implements Paymenta contract with more implementations coming
AssociationDispensingState → Slot, Paymentit finishes this exact sale
Dependencystates use VendingMachine passed as a parameterthey don’t keep a reference to it

That last row is a small, deliberate choice. States receive the machine as a parameter (insert(VendingMachine m, ...)) instead of storing it in a field. They hold no back-reference, so they’re simpler and could be shared between machines.

Step 6The class diagram, built up

Version 1: the machine and its states

VendingMachine- state: MachineState- escrow: List+ insert(d: Denomination): void+ select(code: String): void+ cancel(): void+ dropFinished(ok: boolean): void+ serviceKey(): void«abstract»MachineState+ insert(m, d): void+ select(m, code): void+ cancel(m): void+ dropFinished(m, ok): void+ serviceKey(m): voidIdleStateHasMoneyStateDispensingStateOutOfServiceStatecurrent

Version 2: stock, money, payment and hardware

VendingMachine- display: Display- dispenser: Dispenser- inventory: Inventory- cashBox: CashBox+ beginSale(slot, payment): voidInventory- slots: Map+ slot(code): OptionalSlot- code: String- product: Product- count: int- faulty: boolean+ canSell(): boolean+ takeOne(): voidCashBox- held: Map+ planChange(amount, extra): Optional+ payOut(coins): void«interface»Payment+ complete(m): void+ refund(m): voidCashPayment- inserted: List- change: List«interface»Dispenser+ startDrop(code): void«interface»Display+ show(text): void1..*

Version 3: the whole picture

VendingMachine- state: MachineState- escrow: List- inventory: Inventory- cashBox: CashBox+ insert(d): void+ select(code): void+ cancel(): void+ dropFinished(ok): void+ serviceKey(): void«abstract»MachineState+ insert(m, d): void+ select(m, code): void+ cancel(m): void+ dropFinished(m, ok): void+ serviceKey(m): voidHasMoneyState+ select(m, code): voidDispensingState- slot: Slot- payment: Payment+ dropFinished(m, ok): voidInventory- slots: MapSlot- count: int- faulty: boolean+ canSell(): booleanCashBox- held: Map+ planChange(amount, extra)+ payOut(coins): void«interface»Payment+ complete(m): void+ refund(m): voidCashPaymentcurrent1..*

(Idle and out-of-service states appear in version 1, and the hardware interfaces in version 2. They’re left off here to keep it readable.)

Check it against the requirements

RequirementWhere it lives
FR1: slots with product, price, countInventory, Slot, Product
FR2: insert money, show balanceIdleState.insert, HasMoneyState.insert, escrow
FR3: select and sellHasMoneyState.select → beginSale → DispensingState.dropFinished
FR4: short balance, sold outHasMoneyState.select messages; Slot.canSell
FR5: no change possibleCashBox.planChange returns nothing → explain, stay in Has money
FR6: cancel returns exactly what went inescrow list; HasMoneyState.cancel
FR7: jamDispensingState.dropFinished(false) → refund, markFaulty
FR8: staff serviceOutOfServiceState; staff methods check for it
NFR1: money never lostescrow until the drop; complete or refund, exactly once
NFR2: overlapping eventssynchronized event methods (step 10)
NFR3: extensiblestates and payments are classes behind abstractions
NFR4: testablehardware behind one-method interfaces

Step 7The patterns, and why each one is here

PatternWhereProblem it solvedWithout it
StateMachineState and four statesthe same event means different things in different situationsa switch on a status field in every method
StrategyPaymentfinishing or undoing a sale differs by payment kindif (paidByCard) inside the dispensing logic
Interfaces at the hardware boundaryDisplay, Dispenser, ReturnTraythe software must not depend on one hardware modelno tests without a real machine
Template of defaultsMachineState‘s base methodsmost states refuse most eventsthe same “please wait” copied into every state

State versus Strategy: they look the same

Both patterns are “an object holds an interface, and calls it”. The difference is who changes it and why. A strategy is chosen from outside and usually stays the same for a while: this sale is paid by cash. A state is changed by the states themselves, as a result of events: inserting a coin in Idle moves the machine to Has money. If the interviewer asks “isn’t that just Strategy?”, that’s the answer.

Why not a Singleton

There’s one machine, physically. But a depot runs hundreds of the same software, each instance with its own stock and cash, and tests need fresh machines. Create one per physical machine and pass it its hardware.

Step 8Walk the flows

Watch it run

A vending machine, state by state (B4 costs ₹25)
done
IdleHas moneyDispensingMaintenancestaff onlyBalance₹0

The packet drops, ₹5 comes back, and the machine returns to Idle. Each state only had to know its own rules.

The customer

A sale

CustomerVendingMachineHasMoneyStateCashBoxDispenserSensor1 insert(₹20), insert(₹10)2 select("B4")3 select(m, "B4")4 in stock? enough money?5 planChange(₹5, escrow)6 [₹5]7 beginSale(slot, CashPayment)8 startDrop("B4")9 dropFinished(true)10 take one, move escrow to cash box, pay ₹5, → Idle
  1. Each coin goes into escrow. The screen shows the running balance.
  2. On select, the has-money state checks, in this order: does the code exist, can the slot sell, is the balance enough, can change be made. Each failure has its own message, and the machine stays in Has money so the customer can fix it.
  3. Change is planned before the motor starts. If it isn’t possible, nothing has moved yet.
  4. beginSale switches to a new DispensingState holding the slot and the payment, then starts the motor. The call returns immediately; the motor runs on its own.
  5. Later, the sensor reports. Only now is the sale completed: one item taken from the slot, the escrow moved into the cash box, the change paid out.

Here’s a real run: B4 costs ₹25, and the cash box starts with one ₹5, two ₹2 and one ₹1. Someone also pushes a ₹10 coin into the slot while the coil is turning:

Text
state while dropping: Dispensing, motor started for [B4]
[Cola: ₹25. Insert money, Balance ₹10, Insert ₹15 more, Balance ₹30, Dispensing Cola,
 Please wait, Enjoy your Cola]
returned: [TEN, FIVE], state: Idle

The ₹10 pushed mid-drop came straight back (“Please wait”), along with the ₹5 change.

A jam

VendingMachineDispensingStateSlotCashPaymentReturnTray1 dropFinished(m, false)2 markFaulty()3 refund(m)4 give(inserted money)5 → Idle

Because the money was still in escrow, refunding means handing back exactly what went in. Nothing was ever moved into the cash box, so nothing needs undoing there. That’s the whole point of escrow: the money isn’t the machine’s until the item has dropped.

Text
[Balance ₹20, Dispensing Chips, Sorry, that didn't drop. Refunded, Balance ₹20,
 Sold out. Choose another or cancel] returned [TWENTY]

The next customer who tries A1 sees “sold out”: the slot was marked faulty, so it stops taking money for an item it can’t deliver.

No change possible

The cash box is empty, and someone inserts ₹20 for ₹12 biscuits:

Text
[Balance ₹20, Can't make change. Insert exact money or cancel, Balance ₹30, Balance ₹32,
 Cancelled, Balance ₹10, Balance ₹12, Dispensing Biscuits, Enjoy your Biscuits]

They try adding coins, give up, cancel (getting back their ₹20, ₹10 and ₹2), then insert exact money. That sale works, because no change is needed.

What walking the flows taught us

Found while walkingChange
The motor’s result arrives later, from a sensorDispensing is a real state that waits for dropFinished
A jam must give back exactly what went inMoney stays in escrow until the drop is confirmed
A coin can arrive while dispensingDispensing hands it straight back
Change must be known before the motor turnsplanChange (no side effects) is separate from payOut
The customer’s own coins can be the changeplanChange counts the escrow coins too

The full transition table

The table from step 2, now with exact methods. Every event in every state is accounted for:

FromEventToSide effects
IdleinsertHas moneyescrow += coin
IdleselectIdleshow price
Idleservice keyOut of service—
Has moneyinsertHas moneyescrow += coin
Has moneyselect (can sell)Dispensingstart motor
Has moneyselect (can’t)Has moneyexplain why
Has moneycancelIdlereturn escrow
Has moneyservice keyOut of servicereturn escrow
DispensinginsertDispensingreturn that coin
Dispensingdrop okIdletake item; escrow → box; pay change
Dispensingdrop failedIdlemark faulty; return escrow
Out of serviceinsertOut of servicereturn that coin
Out of serviceservice keyIdle—

Step 9Write the code, bottom-up

Every block below is part of one program that compiles and runs. The outputs on this page come from running it.

9.1 Money and products

Java
enum Denomination {
    ONE(100, false), TWO(200, false), FIVE(500, false), TEN(1000, false),
    TWENTY(2000, true), FIFTY(5000, true), HUNDRED(10000, true);
    final long paise;
    final boolean note;
    Denomination(long paise, boolean note) { this.paise = paise; this.note = note; }
}

record Product(String name, long pricePaise) { }

9.2 Slot and Inventory

Java
final class Slot {
    private final String code;
    private Product product;
    private int count;
    private boolean faulty;

    Slot(String code, Product product, int count) {
        this.code = code; this.product = product; this.count = count;
    }

    String code() { return code; }
    Product product() { return product; }
    int count() { return count; }
    boolean canSell() { return count > 0 && !faulty; }

    void takeOne() {
        if (count == 0) throw new IllegalStateException("slot " + code + " is empty");   // (1)
        count--;
    }

    void markFaulty() { faulty = true; }
    void restock(Product p, int n) { product = p; count = n; faulty = false; }   // (2)
}

final class Inventory {
    private final Map slots = new LinkedHashMap<>();
    void add(Slot s) { slots.put(s.code(), s); }
    Optional slot(String code) { return Optional.ofNullable(slots.get(code)); }
}

Note (1): the state already checked canSell() before the motor turned, so this should never fire. It’s there anyway. If a bug ever gets past the check, failing loudly beats a stock count of −1.

Note (2): restocking clears the faulty flag. Staff fix the coil when they refill it.

9.3 CashBox and planning change

This is the trickiest logic in the problem, and it hides a trap.

The obvious way to make change is greedy: give the biggest coin that fits, repeat. With unlimited coins, greedy always works for Indian coin values. But a machine has limited coins. Say it owes ₹6 and holds one ₹5 coin and three ₹2 coins. Greedy takes the ₹5, then needs ₹1, has none, and gives up. Yet three ₹2 coins would have worked. A greedy machine refuses a sale it could have made, or worse, a sloppy one hands over ₹5 and keeps the customer’s ₹1.

Our planner still tries the largest coin first, but when that leads to a dead end it backs up and tries one fewer, then one fewer again. It’s a small search, called backtracking:

Java
final class CashBox {
    private final Map held = new EnumMap<>(Denomination.class);

    void deposit(List money) {
        for (Denomination d : money) held.merge(d, 1, Integer::sum);
    }

    void payOut(List coins) {
        for (Denomination d : coins) {
            int left = held.getOrDefault(d, 0) - 1;
            if (left < 0) throw new IllegalStateException("no " + d + " left");
            held.put(d, left);
        }
    }

    // Exact change from the coins actually held (plus `extra`, the customer's coins).
    Optional> planChange(long amount, List extra) {
        Map avail = new EnumMap<>(held);
        for (Denomination d : extra) avail.merge(d, 1, Integer::sum);   // (1)
        List coins = new ArrayList<>();
        for (Denomination d : Denomination.values()) if (!d.note) coins.add(0, d);   // (2)
        List out = new ArrayList<>();
        return fill(amount, coins, 0, avail, out) ? Optional.of(out) : Optional.empty();
    }

    private static boolean fill(long left, List coins, int i,
            Map avail, List out) {
        if (left == 0) return true;
        if (i == coins.size()) return false;
        Denomination d = coins.get(i);
        int most = (int) Math.min(avail.getOrDefault(d, 0), left / d.paise);
        for (int k = most; k >= 0; k--) { 3
            for (int j = 0; j < k; j++) out.add(d);
            if (fill(left - k * d.paise, coins, i + 1, avail, out)) return true;
            for (int j = 0; j < k; j++) out.remove(out.size() - 1); 4
        }
        return false;
    }

    List collectNotes() {
        List notes = new ArrayList<>();
        for (Denomination d : Denomination.values()) {
            if (!d.note) continue;
            for (int i = 0; i < held.getOrDefault(d, 0); i++) notes.add(d);
            held.put(d, 0);
        }
        return notes;
    }
}
  1. 3as many as fit, then fewer
  2. 4undo, try fewer

Walk it through for ₹6 with one ₹5 and three ₹2. It tries one ₹10 (none held), then one ₹5: ₹1 left, needs ₹2s and ₹1s, fails. It backs up and tries zero ₹5s: ₹6 left, takes three ₹2s. Done. The real run agrees:

Text
greedy for ₹6: fails
planned for ₹6: [TWO, TWO, TWO]

To be sure the planner never misses an answer, we checked it against a brute-force search that tries every combination, on 20,000 random cash boxes and amounts. Zero disagreements. With only four coin types and small amounts, the search is instant.

Note (1) is the escrow detail from step 4: the customer’s own coins count as available change.

9.4 Payments and hardware

Java
interface Display { void show(String message); }
interface Dispenser { void startDrop(String slotCode); } 1
interface ReturnTray { void give(List money); }

interface Payment {
    void complete(VendingMachine m);
    void refund(VendingMachine m);
}

final class CashPayment implements Payment {
    private final List inserted;
    private final List change;

    CashPayment(List inserted, List change) {
        this.inserted = List.copyOf(inserted); 2
        this.change = List.copyOf(change);
    }

    public void complete(VendingMachine m) {
        m.cashBox().deposit(inserted);
        m.cashBox().payOut(change);
        m.tray().give(change);
    }

    public void refund(VendingMachine m) { m.tray().give(inserted); }
}
  1. 1returns at once
  2. 2a snapshot, not the live escrow list

Note (2): the payment copies the escrow list. The machine clears its escrow after the sale; if the payment held the same list, clearing it would empty the payment’s record too.

9.5 The base state

Java
abstract class MachineState {
    void insert(VendingMachine m, Denomination d) { 1
        m.tray().give(List.of(d));
        m.display().show("Please wait");
    }
    void select(VendingMachine m, String code) { m.display().show("Please wait"); }
    void cancel(VendingMachine m) { }
    void dropFinished(VendingMachine m, boolean ok) { }
    void serviceKey(VendingMachine m) { m.display().show("Please wait"); }
    abstract String name();
}
  1. 1by default: hand it back

Note (1) is the safest default for money: any state that doesn’t say otherwise gives coins straight back. A forgotten override can never swallow a coin.

9.6 Idle

Java
final class IdleState extends MachineState {
    void insert(VendingMachine m, Denomination d) {
        m.escrow().add(d);
        m.display().show("Balance ₹" + m.balance() / 100);
        m.setState(new HasMoneyState());
    }

    void select(VendingMachine m, String code) {
        m.inventory().slot(code).ifPresentOrElse(
            s -> m.display().show(s.product().name() + ": ₹"
                    + s.product().pricePaise() / 100 + ". Insert money"),
            () -> m.display().show("No such item"));
    }

    void serviceKey(VendingMachine m) {
        m.display().show("Out of service");
        m.setState(new OutOfServiceState());
    }

    String name() { return "Idle"; }
}

9.7 Has money: where the sale is decided

Java
final class HasMoneyState extends MachineState {
    void insert(VendingMachine m, Denomination d) {
        m.escrow().add(d);
        m.display().show("Balance ₹" + m.balance() / 100);
    }

    void select(VendingMachine m, String code) {
        Optional found = m.inventory().slot(code);
        if (found.isEmpty()) { m.display().show("No such item"); return; }
        Slot slot = found.get();
        if (!slot.canSell()) {
            m.display().show("Sold out. Choose another or cancel");
            return;
        }
        long price = slot.product().pricePaise();
        long owed = m.balance() - price;
        if (owed < 0) {
            m.display().show("Insert ₹" + (-owed / 100) + " more");
            return;
        }
        Optional> change = m.cashBox().planChange(owed, m.escrow());
        if (change.isEmpty()) {
            m.display().show("Can't make change. Insert exact money or cancel");   // (1)
            return;
        }
        m.beginSale(slot, new CashPayment(m.escrow(), change.get()));
    }

    void cancel(VendingMachine m) {
        m.tray().give(List.copyOf(m.escrow())); 2
        m.escrow().clear();
        m.display().show("Cancelled");
        m.setState(new IdleState());
    }

    void serviceKey(VendingMachine m) { cancel(m); m.setState(new OutOfServiceState()); }

    String name() { return "HasMoney"; }
}
  1. 2exactly what went in

Read select top to bottom: four checks, cheapest first, each with a message that tells the customer what to do. Every failed check returns without changing anything, so the customer stays in Has money with their balance intact.

Note (1): an older version of this design refunded automatically when change wasn’t possible. Staying put is kinder: the customer can add a coin to make the amount exact.

9.8 Dispensing: only the sensor can end it

Java
final class DispensingState extends MachineState {
    private final Slot slot;
    private final Payment payment;

    DispensingState(Slot slot, Payment payment) {
        this.slot = slot;
        this.payment = payment;
    }

    void dropFinished(VendingMachine m, boolean ok) {
        if (ok) {
            slot.takeOne();
            payment.complete(m); 1
            m.display().show("Enjoy your " + slot.product().name());
        } else {
            slot.markFaulty();
            payment.refund(m);
            m.display().show("Sorry, that didn't drop. Refunded");
        }
        m.escrow().clear();
        m.setState(new IdleState());
    }

    String name() { return "Dispensing"; }
}
  1. 1money moves only now

Everything else (insert, select, cancel, service key) uses the base class’s “please wait” behaviour. Cancel is ignored on purpose: once the coil is turning, it’s too late to undo the sale.

9.9 Out of service

Java
final class OutOfServiceState extends MachineState {
    void insert(VendingMachine m, Denomination d) {
        m.tray().give(List.of(d));
        m.display().show("Out of service");
    }
    void select(VendingMachine m, String code) { m.display().show("Out of service"); }
    void serviceKey(VendingMachine m) {
        m.display().show("Ready");
        m.setState(new IdleState());
    }
    String name() { return "OutOfService"; }
}

9.10 The machine

Java
final class VendingMachine {
    private final Inventory inventory;
    private final CashBox cashBox;
    private final Display display;
    private final Dispenser dispenser;
    private final ReturnTray tray;
    private final List escrow = new ArrayList<>();
    private MachineState state = new IdleState();

    VendingMachine(Inventory inventory, CashBox cashBox, Display display,
                   Dispenser dispenser, ReturnTray tray) {
        this.inventory = inventory; this.cashBox = cashBox; this.display = display;
        this.dispenser = dispenser; this.tray = tray;
    }

    // events from the hardware: one at a time
    public synchronized void insert(Denomination d) { state.insert(this, d); }
    public synchronized void select(String code) { state.select(this, code); }
    public synchronized void cancel() { state.cancel(this); }
    public synchronized void dropFinished(boolean ok) { state.dropFinished(this, ok); }
    public synchronized void serviceKey() { state.serviceKey(this); }

    // staff only, and only while out of service
    public synchronized void restock(String code, Product p, int n) {
        requireService();
        inventory.slot(code).orElseThrow().restock(p, n);
    }
    public synchronized void loadCoins(List coins) {
        requireService();
        cashBox.deposit(coins);
    }
    public synchronized List collectNotes() {
        requireService();
        return cashBox.collectNotes();
    }
    private void requireService() {
        if (!(state instanceof OutOfServiceState))
            throw new IllegalStateException("turn the service key first");
    }

    // used by the states
    void setState(MachineState s) { state = s; }
    void beginSale(Slot slot, Payment p) {
        setState(new DispensingState(slot, p)); 1
        display.show("Dispensing " + slot.product().name());
        dispenser.startDrop(slot.code()); 2
    }
    long balance() { return escrow.stream().mapToLong(d -> d.paise).sum(); }
    List escrow() { return escrow; }
    Inventory inventory() { return inventory; }
    CashBox cashBox() { return cashBox; }
    Display display() { return display; }
    ReturnTray tray() { return tray; }
    synchronized String stateName() { return state.name(); }
}
  1. 1switch first…
  2. 2…then start the motor

Notes (1) and (2): the order matters. If the motor started first, a very fast sensor could report “dropped” before the machine had switched to Dispensing, and Has money would ignore the signal. Switch, then start the motor.

9.11 Putting it together

The runs above came from this setup: three slots, a cash box with a little change, and lambdas standing in for the hardware so the test can see everything.

Java
Inventory inv = new Inventory();
inv.add(new Slot("A1", new Product("Chips", 2000), 5));
inv.add(new Slot("B4", new Product("Cola", 2500), 1));
inv.add(new Slot("C2", new Product("Biscuits", 1200), 3));
CashBox box = new CashBox();
box.deposit(List.of(Denomination.FIVE, Denomination.TWO,
                    Denomination.TWO, Denomination.ONE));
VendingMachine m = new VendingMachine(inv, box,
    shown::add, dropped::add, returned::addAll); 1
  1. 1lambdas stand in for hardware

And the staff side:

Text
restock while idle: turn the service key first
notes collected: [TWENTY]
after service: Idle, A1 sellable again: true

Step 10Make it safe when events overlap

Interviewer

Interviewer: It’s one customer at a time. Why worry about threads?

You

You: Because the hardware isn’t one thing. The coin acceptor, keypad, sensor and service key each run their own code and call the machine whenever something happens. Those calls can overlap.

What can go wrong

Suppose the event methods had no lock. Here’s one overlap, slowed down. The customer presses B4 at the same moment the coin slot reports one more coin:

TimeKeypad thread: select("B4")Coin thread: insert(₹10)
1balance is ₹30, plans ₹5 change from the escrow
2adds ₹10 to escrow: now ₹40
3creates CashPayment from the escrow: ₹40
4the sale completes; the customer gets ₹5 change

The customer paid ₹40 and got ₹5 back for a ₹25 item. ₹10 has vanished into the cash box, and NFR1 is broken. Other overlaps are just as bad: the sensor reporting while the machine is still switching states, or the service key turned halfway through a sale.

One lock on the machine

Every event method is synchronized: only one runs at a time, and each sees everything the previous one did. Because every event is quick (no event waits for the motor; startDrop returns at once), holding the lock costs nothing noticeable.

The lock goes on the machine, not on the states, the slots or the cash box. The rules span all of them: a sale reads the escrow, the slot and the cash box, and switches the state. Protecting them separately would allow exactly the in-between moments shown above.

Prove it

Four threads hammer one machine with 80,000 random events (coins and notes, selections, cancels), while a fifth thread plays the sensor, confirming drops and occasionally reporting a jam. At the end we check the one rule that matters most:

money inserted = money now in the cash box + money handed back.

Text
concurrent: drops=9 sold=8 money balances=true state=Idle

The counts of drops and sales change from run to run, because thread timing does. The balance check holds every time. One of the drops in this run was a simulated jam, which is why sales are one fewer than drops: the money for that one came back.

Don’t hold the lock while waiting

If startDrop waited for the motor to finish, it would hold the machine’s lock for two or three seconds, and the coin slot couldn’t even hand back a coin during that time. That’s why the dispenser starts the motor and returns, and the result arrives as a separate dropFinished event. The general rule: never wait for slow hardware, a network, or a person while holding a lock.

Step 11Follow-ups, extended without editing

Card payments

Interviewer

Interviewer: The machine now takes cards. The customer chooses an item, then taps.

Bad solutionif (paidByCard) inside the existing states

Has money and Dispensing both grow card branches, and the next payment method adds more.

Good solutionTreat a card as a kind of payment

Add CardPayment. DispensingState already calls payment.complete() and payment.refund(), so it handles card sales without changing.

Great solutionA new state for the wait, and nothing else changes

Card approval takes a few seconds and can fail. That’s a new situation, so it’s a new state: Awaiting card. Here’s the complete addition:

Java
interface CardReader { void authorize(long amountPaise); } 1
interface CardGateway { void capture(long amount); void release(long amount); }

final class CardPayment implements Payment {
    private final CardGateway gateway;
    private final long amount;
    CardPayment(CardGateway gateway, long amount) {
        this.gateway = gateway;
        this.amount = amount;
    }
    public void complete(VendingMachine m) { gateway.capture(amount); } 2
    public void refund(VendingMachine m) { gateway.release(amount); } 3
}

final class AwaitingCardState extends MachineState {
    private final Slot slot;
    AwaitingCardState(Slot slot) { this.slot = slot; }

    void cardResult(VendingMachine m, boolean approved) {
        if (!approved) {
            m.display().show("Card declined");
            m.setState(new IdleState());
            return;
        }
        m.beginSale(slot, new CardPayment(m.gateway(), slot.product().pricePaise()));
    }

    void cancel(VendingMachine m) {
        m.display().show("Cancelled");
        m.setState(new IdleState());
    }
    String name() { return "AwaitingCard"; }
}
  1. 1result comes back later
  2. 2take the money
  3. 3let the hold go

Card payments work in two steps, just like escrow: the bank first authorizes (puts a hold on the money), and later the shop captures it or releases the hold. That maps exactly onto our design: capture when the item drops, release on a jam.

Besides these new classes, the existing code gains only additions: two new events on the machine (payByCard(code) and cardResult(approved)), two matching do-nothing defaults on MachineState, and IdleState.payByCard, which starts the card wait. No existing state’s logic changes. The real run, with a sale, a jam and a declined card:

Text
card: [Tap your card, Please wait, Dispensing Chips, Enjoy your Chips, Tap your card,
 Dispensing Chips, Sorry, that didn't drop. Refunded, Tap your card, Card declined]
 [authorize 2000, capture 2000, authorize 2000, release 2000, authorize 3000]

Read the second list. Successful sale: authorize, then capture. Jam: authorize, then release: the customer is never charged. Decline: authorize only, and nothing more. The “Please wait” in the middle is a coin someone pushed while the card was being checked; the base class handed it straight back.

Tell the operator when stock runs low

Interviewer

Interviewer: The company wants a message when any slot is down to two items.

This is the Observer pattern: the machine announces events, and whoever’s interested listens. Add a MachineListener interface with methods like sold(slot), slotFaulty(slot) and lowStock(slot), and call the listeners from DispensingState after a sale. A listener that sends a text message, one that writes to a log, and one that updates a dashboard can all be attached without the machine knowing which exist. Send the messages after the event finishes, and never from inside the machine’s lock if they involve the network.

Discounts and price changes

Interviewer

Interviewer: Drinks are 10% off after 9pm.

Prices are currently read straight from Product. Put a PricingStrategy in front: price(product, time). HasMoneyState asks the strategy instead of the product. The first version returns the product’s price unchanged; the evening discount is a new class. This is one small edit to one state, so say so honestly, and point out that it’s the last time pricing rules will ever touch the state code.

Buying several items at once

Interviewer

Interviewer: Customers want to buy two things with one payment.

That changes the model rather than extending it: a basket is a list of slots, change is planned for the total, and dispensing becomes a sequence where some items might drop and others jam. Partial jams mean partial refunds, which the Payment interface would need to support (refund(amount)). Say this, and suggest the simpler alternative most real machines use: after a sale, stay in Has money if there’s balance left, so the customer can buy again with the remainder.

Keeping records

Interviewer

Interviewer: Finance wants every sale recorded, even if the machine loses power.

Write each sale to local storage before moving money and before reporting success, in the same spirit as a database’s transaction log. On power-up, the machine reads the log, finds any sale that started but didn’t finish, and resolves it: a recorded drop with no recorded payment completion means complete it now. This is the same idea as the outbox pattern from the system design course, in miniature.

Mistakes people make on this one

  • A status field with a switch in every method, instead of state classes.
  • A balance number instead of escrow, so a cancel can’t return the coins that went in.
  • Moving money into the cash box before the item drops, so a jam needs messy undoing.
  • Greedy change with limited coins, refusing sales that were possible.
  • *Using double for money.*
  • No dispensing state, so a coin inserted mid-drop gets added to the next customer’s balance.
  • Starting the motor before switching state, so a fast sensor signal is ignored.
  • Separate locks for slots and cash, leaving gaps between them.
  • Holding the lock while the motor turns.
  • *Card payments as if statements* scattered through existing states.

How this answer gets scored

MomentWhat earns creditWhat costs you
RequirementsAsking about change, cancel, jams, staff and concurrent hardwareOnly the happy path
ModellingA state-by-event table before classesJumping to a status field
PatternOne class per state; the machine delegates; defaults in a base classswitch (status) everywhere
MoneyEscrow, whole paise, change planned from real coinsA balance number; double; unlimited change
FailuresJam refunds exactly; faulty slot stops selling; no-change explainedIgnoring them
ConcurrencyOne lock on the machine; never waiting inside it“It’s one customer, so no threads”
ExtensionsCard as one state plus one payment class; listeners for alertsEditing every state for each feature

The whole design on one page

  1. Table first: states (Idle, Has money, Dispensing, Out of service) against events (insert, select, cancel, drop finished, service key).
  2. State pattern: the machine passes every event to its current state; a base class refuses by default and hands coins back.
  3. Escrow: inserted money is held as actual coins and notes until the drop is confirmed. Cancel and jams return it exactly.
  4. Change: planned before the motor starts, from coins really held plus the customer’s own, with a search that backs up when greedy fails.
  5. Payment: complete or refund, so dispensing doesn’t care whether it was cash or card.
  6. Threads: hardware events arrive concurrently; one lock on the machine; never wait for the motor inside it.