The idea
What it is
“Design a vending machine.” It sounds smaller than a parking lot, and the object model genuinely is — six classes, maybe seven. What makes it a real interview question is that its behaviour changes over time. The exact same button press is valid, invalid, or meaningless depending on what just happened.
Press the cola button with no money in: nothing should happen. Press it with ₹30 in: you get a cola and ₹5 back. Press it while the machine is mid-dispense: absolutely nothing should happen, or you have just given away two colas for one payment. That is a state machine, and recognising it in the first two minutes is most of the score.
The whole system in three sentences
The machine sits in exactly one state at a time. Each state decides what insertCoin, select and refund are allowed to do, and which state comes next. Everything else — inventory, prices, change — is bookkeeping around that.
Why this problem exists in the interview set
- It punishes boolean soup. Two flags give four combinations, three give eight, and half of them are nonsense that your code still has to survive. The interviewer wants to see whether you notice.
- It has real money in it. Every ordering mistake is a bug you can describe in rupees — dispense before you check change and the customer is out of pocket.
- It is small enough to finish. Unlike a parking lot, you can write the whole thing in 45 minutes, which means there is nowhere to hide.
Mechanics
How it works
Step 1 · Clarify — 4 minutes
- Coins or notes, which denominations? — “₹5, ₹10, ₹20, ₹50.” This decides the change algorithm.
- Does it give change? — Say yes. A machine without change is a much smaller problem, and the interviewer wants the change conversation.
- What if it cannot make exact change? — “Refuse the sale and return the money.” Getting this rule agreed early stops a bad design later.
- Can you cancel? — Yes, coin return at any time. That is the escape hatch every state needs.
- Restocking, card payments, multi-buy? — Out. Say so and move.
Do not accept “assume unlimited change”
It sounds like a simplification and it deletes the most interesting half of the problem. Push back once — “can I assume the machine has a finite coin bank? It makes the change logic real” — and you have just given yourself something good to demo.
Step 2 · The state machine is the design
Before any class, draw the states and the arrows between them. Four states cover the whole machine. Every arrow is a method call, and every arrow that does not exist is a bug you no longer have to write a guard for.
DISPENSING for a button press, which is why a double-tap cannot give away two colas. Notation: State diagrams.Now look at the same thing as a table. This is the artefact to actually write on the whiteboard, because it is what your code will look like:
Step 3 · Two ways to write it, and why one loses
Everybody's first instinct is flags. It works for about ten minutes.
// what most people type first
private boolean hasMoney;
private boolean dispensing;
private boolean returningChange;
public void select(String code) {
if (dispensing || returningChange) return;
if (!hasMoney) { display("INSERT COIN"); return; }
// ... and every new method repeats this same prelude,
// slightly differently, until two of them disagree
}Three booleans describe eight combinations, but the machine only has four legal states. The other four — “dispensing and returning change at once” — are nonsense your code must still survive, and nothing stops them being set.
state field deletes those rows from the universe rather than from your guards.Now the version that holds up:
interface State {
default State insertCoin(Machine m, int coin) { return this; } // ignored by default
default State select(Machine m, String code) { return this; }
default State refund(Machine m) { return this; }
}
class Idle implements State {
public State insertCoin(Machine m, int coin) {
m.addCredit(coin);
return new HasMoney(); // the transition IS the return value
}
// select() and refund() are simply not overridden — that is the "ignored" cell
}The move that makes it click
A state's method returns the next state. No setState scattered through the code, no flag to forget. If a transition is not written, it does not exist — the empty cells in that table become code you never wrote instead of guards you must remember. This is the State pattern.
How far to actually take this in 45 minutes
A single enum State plus a switch in each method is completely acceptable and much faster to type. It gets you the same exhaustiveness. Mention that separate state classes are where you would go if states grew behaviour of their own, and the interviewer will nod. Do not build four classes for four two-line states unless you have time to spare — that is Pattern overuse & anti-patterns territory.
Step 4 · The class diagram
Slot sitting between Inventory and Product: a Coke is one Product with one price, but slot A1 has a count. Merging them is the most common modelling slip here — restock A1 and you would be editing the drink itself.Making change — and the ordering trap underneath it
Owed ₹35, holding ₹20 ×1, ₹10 ×2, ₹5 ×3. Take the biggest coin you have that still fits, repeat. That is greedy, and it is correct for ordinary currency systems.
The bug this problem is really testing
Check that you can make the change before you dispense the item. Dispense first and then discover the coin bank is empty, and the customer has a ₹15 candy and you are holding their ₹50 with no way to give ₹35 back. Order of operations is the design. Press 💰 Empty the coin bank in the prototype and try it — the item never leaves the shelf.
One purchase, message by message
The follow-ups
- “Add card payments.” → a
PaymentMethodinterface withCoinsandCardimplementations. The state machine does not change: a card swipe is just another way to reachHAS_MONEY. - “Two people press buttons at once.” → in a real machine there is one physical front panel, so serialising at the machine is genuinely correct here — unlike a parking lot with many gates. Say why it is different; that contrast is the point.
- “Restocking and a maintenance mode.” → a fifth state,
SERVICE, that only aMaintenanceKeycan enter. Notice how cheap a new state is once states are real objects. - “Track sales for reporting.” → the machine publishes an event on each sale and a
SalesLoglistens. That is Observer, and it keeps reporting out of the purchase path entirely. - “A product has a discount on Tuesdays.” → pricing becomes a strategy, exactly as in Parking Lot. Same trick, different problem.
How this round is lost
- Boolean soup. Three flags, eight combinations, and the interviewer asking “what happens if
dispensingandreturningChangeare both true?” - Dispensing before verifying change. The single most-caught bug in this problem.
- Merging
ProductandSlot. Then “restock A1” means mutating the definition of Coke. - Money as a
double. ₹0.1 + ₹0.2 is a bad look on a machine that handles cash. Integers, always. - Four state classes for four one-line states, with no working
main()at the end. The pattern is not the deliverable; the machine is.
Interactive prototype
See it. Build it. Break it.
A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.
About this simulation
A real vending machine. Try the wrong thing first: tap a product before paying and the machine refuses you — not with a validation message, but because you are in the wrong state. The pill strip at the top always shows where you are. Insert ₹20 + ₹10, buy the ₹25 cola, and watch one full loop: IDLE → HAS_MONEY → DISPENSING → CHANGE → IDLE, with the item dropping and ₹5 coming back. Then break it on purpose — 💰 Empty the coin bank, insert ₹50, buy the ₹15 candy. The machine refuses before the item drops, because a machine that dispenses and then discovers it cannot pay you back has stolen ₹35.
Hands-on
Try these yourself
Open the prototype above, predict what happens, then verify.
Get refused on purpose
Before doing anything else, tap A1 Cola. Nothing happens — and the explain line tells you why: you are in IDLE, and select() is not an arrow out of IDLE. That is not a validation check, it is a missing transition. Notice the state pill never moved.
Walk one full loop
Insert ₹20 then ₹10 (watch affordable slots turn green the moment you have credit), then buy A1 Cola ₹25. Follow the pill strip: IDLE → HAS_MONEY → DISPENSING → CHANGE → IDLE. The cola drops, ₹5 comes back, and stock ticks from ×2 to ×1.
Find the three refusals
Each is a different failure and a different message. Insert ₹5 and try the ₹35 B2 Cookie — insufficient credit, and your coins stay put. Try B3 Gum — sold out, checked before money moves. Then hit ↩ Coin return with ₹0 credit — a legal no-op, not an error.
Break the coin bank
Press 💰 Empty the coin bank, insert ₹50, and buy the ₹15 A3 Candy. The machine owes ₹35 and cannot build it, so it refuses — and critically, the tray stays empty. Now imagine the same code with the dispense one line earlier.
Watch the bank drain you into that corner
Reset, then buy several cheap items with large coins. Each sale takes small coins out of the bank. Eventually a perfectly ordinary purchase gets refused — the failure builds up over time rather than arriving all at once, which is exactly why it is easy to miss in review.
Write it from memory
Blank file, in this order: enum State → Product and Slot → Inventory.take() → CoinBank.canMake()/withdraw() → insertCoin/select/refund → main(). Then add a SERVICE state for restocking. If adding it forces you to touch select(), your states are not carrying their own behaviour yet.
In practice
When to use it — and what trips people up
When an explicit state machine earns its keep
Not every class needs states. The pattern pays for itself when three things are true at once — and a vending machine hits all three, which is exactly why it is the teaching example.
- The same input means different things at different times.
select()is a purchase, an error, or a no-op depending only on where you are. - Illegal combinations are possible with flags. If your booleans can describe a situation the real machine cannot be in, the flags are wrong.
- New states arrive later.
SERVICE,OUT_OF_ORDER,CARD_PENDING— each is a new class, not a new branch in five existing methods.
Where it does not pay: two states and one transition. A boolean is fine, and dressing it up as a state machine is ceremony. Say which side of that line you are on and why — that judgement is more impressive than the pattern.
Interview variants of this same problem
- Coffee machine — the same skeleton, but the scarce resource is shared ingredients rather than discrete slots, which turns it into a concurrency question. See Coffee Machine.
- ATM — a state machine too, but with authentication in front and two resources that must stay consistent. See ATM.
- Elevator — states plus scheduling. Much harder, and usually reserved for the intermediate tier.
- Traffic light / turnstile — the toy versions of this. If you can do the vending machine, you can do those in ten minutes.
If you only remember one thing
Draw the states and arrows before you write a class. The arrows you draw become methods; the arrows you don't draw become bugs you never have to write a guard for.
What it gives you
- Illegal combinations become unrepresentable — there is no way to be dispensing and refunding at once.
- Each state's rules live in one place, so “what happens if I press this now?” has exactly one answer to read.
- Adding SERVICE or CARD_PENDING is a new class, not a new branch in every existing method.
- Checking change before dispensing falls naturally out of writing the transition as one method with a single exit.
- Small enough to finish completely in 45 minutes, with a demo that shows both the happy path and three refusals.
Common mistakes
- Four classes for four short states is more ceremony than an enum plus a switch, which is often the better call under time pressure.
- Transitions are spread across state classes, so the whole machine is no longer visible on one screen — the diagram becomes required documentation.
- Every state needs access to machine internals (credit, bank, inventory), which pushes you towards a wide package-private surface.
- Greedy change is only correct for canonical denominations; a currency with a 1/3/4 system silently returns a worse answer.
- The model assumes a single physical front panel — it does not generalise to a machine served by several concurrent clients without adding locking.
Reference
Code & further reading
A minimal reference implementation and pointers worth bookmarking.
import java.util.*;
enum Coin {
FIVE(5), TEN(10), TWENTY(20), FIFTY(50);
final int value;
Coin(int v) { this.value = v; }
static Coin[] descending() {
Coin[] c = values().clone();
Arrays.sort(c, Comparator.comparingInt((Coin x) -> x.value).reversed());
return c;
}
}
record Product(String name, int price) {} // price in whole rupees — never a double
class Slot {
private final String code;
private final Product product;
private int count;
Slot(String code, Product product, int count) { this.code = code; this.product = product; this.count = count; }
String code() { return code; }
Product product() { return product; }
boolean inStock() { return count > 0; }
void take() { if (count <= 0) throw new IllegalStateException("empty slot " + code); count--; }
void restock(int n){ count += n; }
}
class Inventory {
private final Map<String, Slot> slots = new LinkedHashMap<>();
void add(Slot s) { slots.put(s.code(), s); }
Optional<Slot> find(String code) { return Optional.ofNullable(slots.get(code)); }
}
class CoinBank {
private final Map<Coin, Integer> held = new EnumMap<>(Coin.class);
CoinBank() { for (Coin c : Coin.values()) held.put(c, 0); }
void deposit(Coin c) { held.merge(c, 1, Integer::sum); }
/** Greedy — correct for canonical currency systems like ₹/$/€. */
Optional<Map<Coin, Integer>> plan(int amount) {
Map<Coin, Integer> give = new EnumMap<>(Coin.class);
int left = amount;
for (Coin c : Coin.descending()) {
int n = Math.min(left / c.value, held.get(c));
if (n > 0) { give.put(c, n); left -= n * c.value; }
}
return left == 0 ? Optional.of(give) : Optional.empty(); // empty == cannot make exact change
}
void withdraw(Map<Coin, Integer> plan) {
plan.forEach((c, n) -> held.merge(c, -n, Integer::sum));
}
}
// ---------- states: each method RETURNS the next state ----------
interface State {
default State insertCoin(VendingMachine m, Coin c) { return this; } // ignored unless overridden
default State select(VendingMachine m, String code) { return this; }
default State refund(VendingMachine m) { return this; }
String name();
}
class Idle implements State {
public String name() { return "IDLE"; }
public State insertCoin(VendingMachine m, Coin c) {
m.acceptCoin(c);
return new HasMoney();
}
// select() and refund() deliberately NOT overridden — those cells are "do nothing"
}
class HasMoney implements State {
public String name() { return "HAS_MONEY"; }
public State insertCoin(VendingMachine m, Coin c) {
m.acceptCoin(c);
return this; // stay here, credit grows
}
public State select(VendingMachine m, String code) {
Slot slot = m.inventory().find(code).orElse(null);
if (slot == null) { m.display("INVALID CODE"); return this; }
if (!slot.inStock()) { m.display("SOLD OUT"); return this; }
int price = slot.product().price();
if (m.credit() < price) { m.display("NEED ₹" + (price - m.credit()) + " MORE"); return this; }
int due = m.credit() - price;
// ---- every check that can fail happens BEFORE the first irreversible step ----
Optional<Map<Coin, Integer>> plan = due == 0 ? Optional.of(Map.of()) : m.bank().plan(due);
if (plan.isEmpty()) { m.display("EXACT CHANGE ONLY"); return this; }
slot.take(); // irreversible from here on
m.deductCredit(price);
m.dispense(slot.product());
m.bank().withdraw(plan.get());
m.returnCoins(plan.get());
m.clearCredit();
return new Idle();
}
public State refund(VendingMachine m) {
m.bank().plan(m.credit()).ifPresent(p -> { m.bank().withdraw(p); m.returnCoins(p); });
m.clearCredit();
return new Idle();
}
}
class VendingMachine {
private final Inventory inventory = new Inventory();
private final CoinBank bank = new CoinBank();
private State state = new Idle();
private int credit = 0;
Inventory inventory() { return inventory; }
CoinBank bank() { return bank; }
int credit() { return credit; }
String state() { return state.name(); }
void acceptCoin(Coin c) { credit += c.value; bank.deposit(c); }
void deductCredit(int n) { credit -= n; }
void clearCredit() { credit = 0; }
void display(String msg) { System.out.println(" [display] " + msg); }
void dispense(Product p) { System.out.println(" [tray] " + p.name()); }
void returnCoins(Map<Coin, Integer> coins) {
if (!coins.isEmpty()) System.out.println(" [tray] change " + coins);
}
// the only three inputs — each just forwards to the current state
void insertCoin(Coin c) { state = state.insertCoin(this, c); }
void select(String code) { state = state.select(this, code); }
void refund() { state = state.refund(this); }
}
public class Main {
public static void main(String[] args) {
VendingMachine m = new VendingMachine();
m.inventory().add(new Slot("A1", new Product("Cola", 25), 2));
m.inventory().add(new Slot("A3", new Product("Candy", 15), 1));
m.inventory().add(new Slot("B3", new Product("Gum", 5), 0));
System.out.println("select before paying, state=" + m.state());
m.select("A1"); // ignored — no arrow out of IDLE
System.out.println("still " + m.state());
m.insertCoin(Coin.TWENTY);
m.insertCoin(Coin.TEN);
System.out.println("credit ₹" + m.credit() + ", state=" + m.state());
m.select("A1"); // cola + ₹5 change
System.out.println("after sale, state=" + m.state() + ", credit ₹" + m.credit());
m.insertCoin(Coin.FIFTY);
m.select("A3"); // ₹35 change — bank may not have it
m.refund();
System.out.println("final state=" + m.state());
}
}References & further reading
6 sources- Docsrefactoring.guru
State pattern — Refactoring Guru
The pattern this problem exists to teach, with the same “object changes behaviour when its state changes” framing.
- Articlegithub.com
awesome-low-level-design — Vending Machine
A second take on the same problem — compare its state set against yours.
- Articleen.wikipedia.org
Coin change — canonical coin systems
Why greedy is safe for ₹/$/€ and where it stops being safe. The one-paragraph version is enough for the interview.
- Book
Head First Design Patterns — the State chapter
Builds a gumball machine from booleans to states, step by step. It is essentially this lesson in book form.
- Docsrefactoring.com
Refactoring: Replace Type Code with State/Strategy
The mechanical recipe for getting from a flag-based version to a state-based one without breaking anything.
- Docserlang.org
State machines in practice — Erlang/OTP gen_statem docs
How an ecosystem that takes state machines seriously structures them. Useful vocabulary: state, event, action, timeout.
Knowledge check
Did it land?
Quick questions, answers revealed on submit. Sign in to save your best score.
question 01 / 08
You model the machine with three booleans: hasMoney, dispensing, returningChange. What is the core problem?
question 02 / 08
A customer inserts ₹50 and selects a ₹15 candy, but the coin bank cannot make ₹35. What must the machine do?
question 03 / 08
Why should Product and Slot be separate classes?
question 04 / 08
In the state-object design, what should Idle.select(machine, code) do?
question 05 / 08
Greedy change (biggest coin first) is used here. When would that give a worse answer than necessary?
question 06 / 08
The interviewer asks you to add card payments. What is the smallest correct change?
question 07 / 08
Why does modelling DISPENSING as a real state matter, even though it lasts under a second?
question 08 / 08
With 20 minutes left and no working demo, which is the right call?
0/8 answered