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:
| Step | What you do | What comes out of it |
|---|---|---|
| 1. Understand the problem | Ask questions, write down requirements | What the machine must do, and what can go wrong |
| 2. Break it into entities | Nouns, the status-flag trap, a state-by-event table | Classes, and the table that is the design |
| 3. Choose a direction | Top-down for designing, bottom-up for coding | The order you’ll work in |
| 4. Design each class | Decide what each class knows and does | A card per class |
| 5. Connect the classes | Pick the right relationships | Who owns whom |
| 6. Draw the diagram | Build it up in three versions | The complete class diagram |
| 7. Name the patterns | State, a Payment strategy, hardware interfaces | And why each is there |
| 8. Walk the flows | A sale, a cancel, a jam, call by call | Escrow, and why dispensing is a real state |
| 9. Write the code | Bottom-up, one class at a time | Code that compiles and runs |
| 10. Make it safe | Events from several threads | One lock, and a test that counts every paisa |
| 11. Extend it | Card payments, alerts, pricing | New 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: Design a vending machine.
You: Sure. First, what does it sell, and how is it laid out?
Interviewer: Snacks and drinks in numbered slots like A1 or B4. Each slot holds one product with one price.
You: How do people pay?
Interviewer: Coins of ₹1, ₹2, ₹5 and ₹10, and notes of ₹20, ₹50 and ₹100.
You: Does it give change?
Interviewer: Yes, in coins.
You: What if it can’t make the change?
Interviewer: It shouldn’t short-change anyone. Tell them, and let them put in exact money or cancel.
You: Can the customer cancel?
Interviewer: Yes, any time before the item drops. They get back what they put in.
You: Can the item fail to drop?
Interviewer: It happens. The motor turns, and a sensor at the flap says whether something fell.
You: Anything for staff?
Interviewer: A service key. Staff restock, load coins for change, and collect notes.
You: Will it take cards one day?
Interviewer: Probably next year. Not now.
You: Last one. Is it one customer at a time?
Interviewer: Yes, but the coin slot, keypad, sensor and service key are separate pieces of hardware. They can all send signals at any moment.
| Question | What the answer decided |
|---|---|
| Slots with one product each | Slot holds a product and a count; Inventory holds slots by code. |
| Coins and notes | One Denomination enum, with a flag for notes. |
| Change in coins, never short-change | Change must be planned from coins really held, before anything drops. |
| Cancel any time before the drop | Inserted money is kept separate until the sale completes: escrow. |
| The item can fail to drop | Dispensing takes time and has two outcomes. It’s a real state, not a step. |
| Staff key | A maintenance state, and staff actions that only work in it. |
| Cards next year | Payment sits behind an interface now, so cards can be added later. |
| Separate hardware, any moment | Events arrive on different threads. The machine must handle them one at a time. |
Functional requirements
- FR1. The machine has slots, each with a product, a price and a count.
- FR2. Customers insert coins and notes. The screen shows the balance.
- 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.
- FR4. If the balance is short, the screen says how much more. If the slot is empty, it says sold out.
- FR5. If change can’t be made from the coins inside, the sale is refused and the customer can add exact money or cancel.
- FR6. Cancel, any time before the drop, returns exactly the money inserted.
- FR7. If the item fails to drop, the customer gets their money back and that slot stops selling.
- FR8. With the service key, staff can restock slots, load coins and collect notes. Customers can’t buy meanwhile.
Non-functional requirements
- NFR1: Money is never lost or invented. At every moment, money in = money in the cash box + money handed back.
- 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.
- NFR3: Easy to extend. Card payments, new states and new rules should mean adding classes, not editing working ones.
- 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
| Assumption | Why it’s reasonable |
|---|---|
Prices and money are whole paise, stored as long | No rounding errors, ever |
| Change is given only in coins | Machines rarely dispense notes |
| Notes are accepted but never used for change | They go into a separate locked box |
| One item per sale | Baskets are a follow-up |
| The motor runs on its own; the sensor reports back later | That’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.
| Noun | Own data? | Own rules? | Becomes |
|---|---|---|---|
| Machine | everything below | what each event means right now | VendingMachine class |
| Slot | code, product, count, faulty? | can’t sell when empty or faulty | Slot class |
| Inventory | slots by code | — | Inventory class |
| Product | name, price | — | Product record |
| Coin, note | value, is it a note? | — | Denomination enum |
| Money inserted (balance) | the actual coins and notes | returned exactly on cancel | an escrow list on the machine |
| Cash box | coins and notes held | can only pay out what it has; plans change | CashBox 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:
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()- 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 → | Idle | Has money | Dispensing | Out of service |
|---|---|---|---|---|
| insert coin/note | hold it; → Has money | hold it; show balance | give it straight back | give it straight back |
| select slot | show the price | sell if possible, else explain | “please wait” | “out of service” |
| cancel | nothing to do | return escrow; → Idle | too late: ignore | nothing to do |
| drop finished | (can’t happen) | (can’t happen) | success: complete sale; jam: refund; → Idle | (can’t happen) |
| service key | → Out of service | return 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 case | Who starts it | What happens |
|---|---|---|
| UC1: Buy with cash | Customer | insert money, select, plan change, start the motor, sensor confirms, complete the sale |
| UC2: Cancel | Customer | return exactly what was inserted |
| UC3: Can’t sell | Customer | sold out, short balance, or no change: explain, stay ready |
| UC4: Jam | Sensor | refund, mark the slot faulty |
| UC5: Service | Staff | key in, restock, load coins, collect notes, key out |
Give each job to the class that knows the most
| Job | Information needed | Owner |
|---|---|---|
| Decide what an event means right now | the current situation | the current state object |
| Can this slot sell? Take one item | count, faulty flag | Slot |
| Find a slot by code | all slots | Inventory |
| Can we make ₹X change? Pay it out | coins held | CashBox |
| What did this customer put in? | the escrow list | VendingMachine |
| Finish or undo a payment | how it was paid | Payment (cash today, card later) |
| Show text, turn the motor, return money | the hardware | Display, Dispenser, ReturnTray |
CRC cards
| Class | Responsibilities | Collaborators |
|---|---|---|
VendingMachine | receive events, pass them to the current state; hold escrow and current state | MachineState, Inventory, CashBox, hardware |
MachineState + 4 states | decide what each event means in this situation; switch state | VendingMachine |
Slot, Inventory | stock per code; sellable or not | Product |
CashBox | hold money; plan exact change; pay out | Denomination |
Payment, CashPayment | complete or refund one sale | CashBox, ReturnTray |
Find what will change
| Will change | Hint | Becomes |
|---|---|---|
| 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 itself | different models, and tests | Display, 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.
| Knows | Does | Doesn’t do |
|---|---|---|
| current state | insert, select, cancel, dropFinished, serviceKey | decide what an event means (the state does) |
| inventory, cash box | staff actions: restock, loadCoins, collectNotes | plan change (the cash box does) |
| escrow: the money inserted this time | beginSale(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.
| State | Overrides | Main rule |
|---|---|---|
IdleState | insert, select, service key | first coin starts a session; select only shows the price |
HasMoneyState | insert, select, cancel, service key | the sale decision lives here |
DispensingState | drop finished | only the sensor can end this state |
OutOfServiceState | insert, select, service key | refuses 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 knows | Slot does |
|---|---|
| code, product, count, faulty | canSell(): 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.
| Knows | Does |
|---|---|
| count of each denomination | deposit(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
| Relationship | In this design | Why that kind |
|---|---|---|
| Composition | machine owns its escrow list | the escrow means nothing outside this machine |
| Aggregation | machine holds inventory and cash box | created outside and passed in; could be inspected or swapped |
| Association | machine → current MachineState | changes over time |
| Inheritance | 4 states extend MachineState | shared default behaviour |
| Realization | CashPayment implements Payment | a contract with more implementations coming |
| Association | DispensingState → Slot, Payment | it finishes this exact sale |
| Dependency | states use VendingMachine passed as a parameter | they 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
Version 2: stock, money, payment and hardware
Version 3: the whole picture
(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
| Requirement | Where it lives |
|---|---|
| FR1: slots with product, price, count | Inventory, Slot, Product |
| FR2: insert money, show balance | IdleState.insert, HasMoneyState.insert, escrow |
| FR3: select and sell | HasMoneyState.select → beginSale → DispensingState.dropFinished |
| FR4: short balance, sold out | HasMoneyState.select messages; Slot.canSell |
| FR5: no change possible | CashBox.planChange returns nothing → explain, stay in Has money |
| FR6: cancel returns exactly what went in | escrow list; HasMoneyState.cancel |
| FR7: jam | DispensingState.dropFinished(false) → refund, markFaulty |
| FR8: staff service | OutOfServiceState; staff methods check for it |
| NFR1: money never lost | escrow until the drop; complete or refund, exactly once |
| NFR2: overlapping events | synchronized event methods (step 10) |
| NFR3: extensible | states and payments are classes behind abstractions |
| NFR4: testable | hardware behind one-method interfaces |
Step 7The patterns, and why each one is here
| Pattern | Where | Problem it solved | Without it |
|---|---|---|---|
| State | MachineState and four states | the same event means different things in different situations | a switch on a status field in every method |
| Strategy | Payment | finishing or undoing a sale differs by payment kind | if (paidByCard) inside the dispensing logic |
| Interfaces at the hardware boundary | Display, Dispenser, ReturnTray | the software must not depend on one hardware model | no tests without a real machine |
| Template of defaults | MachineState‘s base methods | most states refuse most events | the 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 sale
- Each coin goes into escrow. The screen shows the running balance.
- 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.
- Change is planned before the motor starts. If it isn’t possible, nothing has moved yet.
beginSaleswitches to a newDispensingStateholding the slot and the payment, then starts the motor. The call returns immediately; the motor runs on its own.- 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:
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: IdleThe ₹10 pushed mid-drop came straight back (“Please wait”), along with the ₹5 change.
A jam
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.
[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:
[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 walking | Change |
|---|---|
| The motor’s result arrives later, from a sensor | Dispensing is a real state that waits for dropFinished |
| A jam must give back exactly what went in | Money stays in escrow until the drop is confirmed |
| A coin can arrive while dispensing | Dispensing hands it straight back |
| Change must be known before the motor turns | planChange (no side effects) is separate from payOut |
| The customer’s own coins can be the change | planChange 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:
| From | Event | To | Side effects |
|---|---|---|---|
| Idle | insert | Has money | escrow += coin |
| Idle | select | Idle | show price |
| Idle | service key | Out of service | — |
| Has money | insert | Has money | escrow += coin |
| Has money | select (can sell) | Dispensing | start motor |
| Has money | select (can’t) | Has money | explain why |
| Has money | cancel | Idle | return escrow |
| Has money | service key | Out of service | return escrow |
| Dispensing | insert | Dispensing | return that coin |
| Dispensing | drop ok | Idle | take item; escrow → box; pay change |
| Dispensing | drop failed | Idle | mark faulty; return escrow |
| Out of service | insert | Out of service | return that coin |
| Out of service | service key | Idle | — |
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
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
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:
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;
}
}
- > planChange(long amount, List
- 3as many as fit, then fewer
- 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:
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
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); }
} - 1returns at once
- 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
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();
}- 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
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
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"; }
}
- > change = m.cashBox().planChange(owed, m.escrow());
if (change.isEmpty()) {
m.display().show("Can't make change. Insert exact money or cancel");
}
m.beginSale(slot, new CashPayment(m.escrow(), change.get()));
}
void cancel(VendingMachine m) {
m.tray().give(List.copyOf(m.escrow()));
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"; }
}
- 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
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"; }
}- 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
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
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(); }
} - 1switch first…
- 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.
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- 1lambdas stand in for hardware
And the staff side:
restock while idle: turn the service key first
notes collected: [TWENTY]
after service: Idle, A1 sellable again: trueStep 10Make it safe when events overlap
Interviewer: It’s one customer at a time. Why worry about threads?
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:
| Time | Keypad thread: select("B4") | Coin thread: insert(₹10) |
|---|---|---|
| 1 | balance is ₹30, plans ₹5 change from the escrow | |
| 2 | adds ₹10 to escrow: now ₹40 | |
| 3 | creates CashPayment from the escrow: ₹40 | |
| 4 | the 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.
concurrent: drops=9 sold=8 money balances=true state=IdleThe 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: 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:
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"; }
}- 1result comes back later
- 2take the money
- 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:
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: 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: 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: 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: 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
doublefor 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
ifstatements* scattered through existing states.
How this answer gets scored
| Moment | What earns credit | What costs you |
|---|---|---|
| Requirements | Asking about change, cancel, jams, staff and concurrent hardware | Only the happy path |
| Modelling | A state-by-event table before classes | Jumping to a status field |
| Pattern | One class per state; the machine delegates; defaults in a base class | switch (status) everywhere |
| Money | Escrow, whole paise, change planned from real coins | A balance number; double; unlimited change |
| Failures | Jam refunds exactly; faulty slot stops selling; no-change explained | Ignoring them |
| Concurrency | One lock on the machine; never waiting inside it | “It’s one customer, so no threads” |
| Extensions | Card as one state plus one payment class; listeners for alerts | Editing every state for each feature |
The whole design on one page
- Table first: states (Idle, Has money, Dispensing, Out of service) against events (insert, select, cancel, drop finished, service key).
- State pattern: the machine passes every event to its current state; a base class refuses by default and hands coins back.
- Escrow: inserted money is held as actual coins and notes until the drop is confirmed. Cancel and jams return it exactly.
- Change: planned before the motor starts, from coins really held plus the customer’s own, with a search that backs up when greedy fails.
- Payment:
completeorrefund, so dispensing doesn’t care whether it was cash or card. - Threads: hardware events arrive concurrently; one lock on the machine; never wait for the motor inside it.
Stay in the loop
Get new problems and walkthroughs by email
One short email when something new is published: a deep-dive walkthrough, an LLD problem, or a real outage retold. No spam, unsubscribe any time.