Intermediate · 7 min read · updated 14 November 2026
Quick answer
How do you design a vending machine in a low-level design interview?
Model a VendingMachine that holds a balance, its items and a current State. Each state (Idle, HasMoney, Dispensing, OutOfStock) handles insert, select and dispense in its own way and moves the machine to the next state. Change is made greedily from the largest coin down, which is optimal for canonical coin sets such as 10, 5, 2 and 1.
A vending machine is the textbook case for the State pattern: the same button means different things depending on what the machine is doing, and that is easiest to model as one class per state.
Requirements
Items in slots (A1, A2), each with a price and a stock count.
Accept coins of 1, 2, 5 and 10; keep a running balance.
Select an item: refuse it if unpaid or sold out, otherwise dispense it and return change.
When every slot is empty, return any coin inserted.
Out of scope unless asked: notes, cards, a refund button, restocking.
Classes and relations
The vending machine's classes: the State pattern, step by step
One VendingMachine holds the money inserted so far and the current state. Customers only ever call the machine: insert a coin, select a code.
0:01 / 0:16Step 1 of 5
Transcript · 5 steps
The machine delegates every event to its current State and holds no if-chain on a status.
States decide the transition: each calls machine.move(next).
The machine starts in Idle with no money, one bag of chips at 15 and one juice at 22. Every customer action is an event handed to the current state.
0:01 / 0:31Step 1 of 12
Transcript · 12 steps
State
insert coin
select item
Idle
add it, go to HasMoney
ignored
HasMoney
add it
if paid, go to Dispensing
Dispensing
returned (busy)
ignored
OutOfStock
returned
ignored
select(A2): the state decides, the machine only delegates
With 30 inserted, the customer presses A2. The machine does not decide anything itself: it hands the event to its current state.
0:01 / 0:21Step 1 of 9
Transcript · 9 steps
Making change
Making change: greedy over 10, 5, 2 and 1, and where greedy fails
The juice costs 22 and 30 is in, so the machine owes 8. Greedy change tries the coins from the largest down and takes each while it still fits.
0:01 / 0:16Step 1 of 6
Transcript · 6 steps
Greedy: largest coin that fits, repeat. Optimal for 10/5/2/1.
Not optimal for every coin set (4/3/1 above); then use dynamic programming.
A real machine also has limited coins: check it can pay the change before dispensing, or refuse the sale.
The code
The four states, greedy change, and one scenario: an underpaid selection, a sale with change, a sale that empties the machine, and a coin returned when sold out.
C++JavaPythonJavaScript
Copy
#include<iostream>#include<map>#include<string>usingnamespace std;structItem{ string name;int price;int stock;};structMachine;// Each state answers the events itself; the machine only points at the current one.structState{virtual string name()const=0;virtualvoidinsert(Machine& m,int coin)=0;virtualvoidselect(Machine& m,const string& code){ cout <<"select "<< code <<": ignored in "<<name()<<"\n";}virtualvoiddispense(Machine& m){}};structIdle:State{ string name()constoverride{return"Idle";}voidinsert(Machine& m,int coin)override;};structHasMoney:State{
string name()constoverride{return"HasMoney";}voidinsert(Machine& m,int coin)override;voidselect(Machine& m,const string& code)override;};structDispensing:State{
string name()constoverride{return"Dispensing";}voidinsert(Machine& m,int coin)override{ cout <<"insert "<< coin <<": busy, returned "<< coin <<"\n";}voiddispense(Machine& m)override;};structOutOfStock:State{
string name()constoverride{return"OutOfStock";}voidinsert(Machine& m,int coin)override{ cout <<"insert "<< coin <<": sold out, returned "<< coin <<"\n";}};// States hold no data of their own, so one shared instance of each is enough.
Idle IDLE; HasMoney HAS_MONEY; Dispensing DISPENSING; OutOfStock OUT_OF_STOCK;structMachine{
map<string, Item> items;
State* state =&IDLE;int balance =0;
Item* selected =nullptr;voidmove(State* next,const string& event,const string& note){
cout << event <<": "<< state->name()<<" -> "<< next->name()<<(note.empty()?"":", "+ note)<<"\n";
state = next;}// Greedy: the largest coin that fits, again and again (optimal for 10/5/2/1).static string change(int amount){
string out;for(int c :{10,5,2,1})while(amount >= c){ out +=(out.empty()?"":"+")+to_string(c); amount -= c;}return out.empty()?"none": out;}};voidIdle::insert(Machine& m,int coin){ m.balance += coin; m.move(&HAS_MONEY,"insert "+to_string(coin),"balance "+to_string(m.balance));}voidHasMoney::insert(Machine& m,int coin){ m.balance += coin; cout <<"insert "<< coin <<": balance "<< m.balance <<"\n";}voidHasMoney::select(Machine& m,const string& code){
Item& item = m.items.at(code);if(item.stock ==0){ cout <<"select "<< code <<": "<< item.name <<" sold out\n";return;}if(m.balance < item.price){
cout <<"select "<< code <<": "<< item.name <<" costs "<< item.price <<", insert "<< item.price - m.balance <<" more\n";return;}
m.selected =&item;
m.move(&DISPENSING,"select "+ code,"");
m.state->dispense(m);}voidDispensing::dispense(Machine& m){
Item& item =*m.selected;
item.stock--;
string change =Machine::change(m.balance - item.price);
m.balance =0;bool empty =true;for(auto&[code, i]: m.items)if(i.stock >0) empty =false;
m.move(empty ?(State*)&OUT_OF_STOCK :&IDLE,"dispense", item.name +", change "+ change);}intmain(){
Machine m;
m.items["A1"]={"Chips",15,1}; m.items["A2"]={"Juice",22,1};
m.state->insert(m,10); m.state->select(m,"A1"); m.state->insert(m,10); m.state->select(m,"A1");
m.state->insert(m,10); m.state->insert(m,10); m.state->insert(m,10); m.state->select(m,"A2");
m.state->insert(m,10);}
Why is the State pattern used for a vending machine?
What a coin or a button does depends on what the machine is doing: a coin in Idle starts a sale, in OutOfStock it is returned. The State pattern puts each state's answers in its own class, replacing a growing chain of if-else on a status field.
What are the states of a vending machine?
Idle (waiting for money), HasMoney (collecting coins and waiting for a selection), Dispensing (dropping the item and the change) and OutOfStock (nothing left to sell). Real designs add Maintenance or a cancel path that refunds the balance.
How does a vending machine calculate change?
It subtracts the price from the balance and pays the rest with the largest coin that fits, repeatedly. That greedy rule gives the fewest coins for canonical sets like 10, 5, 2, 1, but not for every set; with limited coins in the machine, a dynamic-programming search is safer.
What happens if the customer cannot pay the full price?
HasMoney refuses the selection, tells the customer how much more is needed and stays in HasMoney. A cancel button returns the whole balance and moves the machine back to Idle.
How do you add a new state such as Maintenance?
Write one new class that implements the State interface and decide which existing states move into it. The machine and the other states' logic stay as they are, which is the open-closed principle at work.
Vending machine low-level design: the State pattern over Idle, HasMoney, Dispensing and OutOfStock, greedy change-making, a class diagram and runnable code.
How do you design a vending machine in a low-level design interview?
Model a VendingMachine that holds a balance, its items and a current State. Each state (Idle, HasMoney, Dispensing, OutOfStock) handles insert, select and dispense in its own way and moves the machine to the next state. Change is made greedily from the largest coin down, which is optimal for canonical coin sets such as 10, 5, 2 and 1.
A vending machine is the textbook case for the State pattern: the same button means different things depending on what the machine is doing, and that is easiest to model as one class per state.
Requirements
Items in slots (A1, A2), each with a price and a stock count.
Accept coins of 1, 2, 5 and 10; keep a running balance.
Select an item: refuse it if unpaid or sold out, otherwise dispense it and return change.
When every slot is empty, return any coin inserted.
Out of scope unless asked: notes, cards, a refund button, restocking.
Classes and relations
The vending machine's classes: the State pattern, step by step.
One VendingMachine holds the money inserted so far and the current state. Customers only ever call the machine: insert a coin, select a code.
The machine owns its items, each with a name, a price and a stock count, keyed by a slot code such as A1.
Every event is passed on to the current State object. What a coin or a button does depends on the state, so the answer lives in the state, not in an if-chain.
Idle takes the first coin and moves to HasMoney. HasMoney adds coins, refuses a selection it cannot pay for and moves to Dispensing when it can.
Dispensing drops the item and the change, then moves to Idle, or to OutOfStock when nothing is left; OutOfStock returns every coin. A new state is a new class.
The machine delegates every event to its current State and holds no if-chain on a status.
States decide the transition: each calls machine.move(next).
The vending machine's states, one event at a time.
The machine starts in Idle with no money, one bag of chips at 15 and one juice at 22. Every customer action is an event handed to the current state.
insert 10: Idle accepts the coin and moves to HasMoney. The balance is now 10.
select A1: chips cost 15 but only 10 is in, so HasMoney stays put and asks for 5 more — no item, no state change.
insert 10 in HasMoney: the coin is added and the state stays the same; the balance is 20.
select A1: the balance of 20 covers the price, so HasMoney hands over to Dispensing, which acts at once.
Dispensing drops the chips and returns 5 in change; juice is still in stock, so the machine goes back to Idle.
insert 10 on an Idle machine again: Idle takes the coin and moves to HasMoney, balance 10.
Another coin in HasMoney: no transition, the balance just grows to 20.
Another coin in HasMoney: no transition, the balance just grows to 30.
select A2: the balance of 30 covers the price, so HasMoney hands over to Dispensing, which acts at once.
Dispensing drops the juice and returns 5 + 2 + 1 = 8 in coins. Nothing is left in stock, so the machine moves to OutOfStock.
insert 10 in OutOfStock: the state returns the coin at once, which is the whole point of having a state for it.
State
insert coin
select item
Idle
add it, go to HasMoney
ignored
HasMoney
add it
if paid, go to Dispensing
Dispensing
returned (busy)
ignored
OutOfStock
returned
ignored
select(A2): the state decides, the machine only delegates.
With 30 inserted, the customer presses A2. The machine does not decide anything itself: it hands the event to its current state.
The current state is HasMoney, so HasMoney.select runs.
HasMoney checks the item: is any left, and does the balance cover the price?
Juice costs 22 and 1 is left; 30 covers it.
HasMoney switches the machine to Dispensing — the transition is the state's call, not the machine's.
The machine asks the new state to act. Dispensing reduces the stock and drops the juice.
Juice's stock falls to 0. With chips already gone, nothing is left in the machine.
Dispensing reads the stock and moves the machine to OutOfStock rather than Idle.
The customer gets the juice and 5+2+1 in change; any coin inserted now comes straight back.
Making change
Making change: greedy over 10, 5, 2 and 1, and where greedy fails.
The juice costs 22 and 30 is in, so the machine owes 8. Greedy change tries the coins from the largest down and takes each while it still fits.
10 is more than the 8 owed, so greedy skips it and moves to the next smaller coin.
5 fits in what is still owed, so it goes in the tray; 3 is still owed.
2 fits in what is still owed, so it goes in the tray; 1 is still owed.
1 fits in what is still owed, so it goes in the tray; nothing is owed now: change is 5 + 2 + 1, the fewest coins possible here.
Greedy is optimal only for some coin sets. With coins 4, 3 and 1, greedy pays 6 as 4 + 1 + 1 (3 coins) while 3 + 3 needs 2; a general machine uses dynamic programming.
Greedy: largest coin that fits, repeat. Optimal for 10/5/2/1.
Not optimal for every coin set (4/3/1 above); then use dynamic programming.
A real machine also has limited coins: check it can pay the change before dispensing, or refuse the sale.
The code
The four states, greedy change, and one scenario: an underpaid selection, a sale with change, a sale that empties the machine, and a coin returned when sold out.
#include <iostream>
#include <map>
#include <string>
using namespace std;
struct Item { string name; int price; int stock; };
struct Machine;
// Each state answers the events itself; the machine only points at the current one.
struct State {
virtual string name() const = 0;
virtual void insert(Machine& m, int coin) = 0;
virtual void select(Machine& m, const string& code) { cout << "select " << code << ": ignored in " << name() << "\n"; }
virtual void dispense(Machine& m) {}
};
struct Idle : State { string name() const override { return "Idle"; } void insert(Machine& m, int coin) override; };
struct HasMoney : State {
string name() const override { return "HasMoney"; }
void insert(Machine& m, int coin) override;
void select(Machine& m, const string& code) override;
};
struct Dispensing : State {
string name() const override { return "Dispensing"; }
void insert(Machine& m, int coin) override { cout << "insert " << coin << ": busy, returned " << coin << "\n"; }
void dispense(Machine& m) override;
};
struct OutOfStock : State {
string name() const override { return "OutOfStock"; }
void insert(Machine& m, int coin) override { cout << "insert " << coin << ": sold out, returned " << coin << "\n"; }
};
// States hold no data of their own, so one shared instance of each is enough.
Idle IDLE; HasMoney HAS_MONEY; Dispensing DISPENSING; OutOfStock OUT_OF_STOCK;
struct Machine {
map<string, Item> items;
State* state = &IDLE;
int balance = 0;
Item* selected = nullptr;
void move(State* next, const string& event, const string& note) {
cout << event << ": " << state->name() << " -> " << next->name() << (note.empty() ? "" : ", " + note) << "\n";
state = next;
}
// Greedy: the largest coin that fits, again and again (optimal for 10/5/2/1).
static string change(int amount) {
string out;
for (int c : {10, 5, 2, 1}) while (amount >= c) { out += (out.empty() ? "" : "+") + to_string(c); amount -= c; }
return out.empty() ? "none" : out;
}
};
void Idle::insert(Machine& m, int coin) { m.balance += coin; m.move(&HAS_MONEY, "insert " + to_string(coin), "balance " + to_string(m.balance)); }
void HasMoney::insert(Machine& m, int coin) { m.balance += coin; cout << "insert " << coin << ": balance " << m.balance << "\n"; }
void HasMoney::select(Machine& m, const string& code) {
Item& item = m.items.at(code);
if (item.stock == 0) { cout << "select " << code << ": " << item.name << " sold out\n"; return; }
if (m.balance < item.price) {
cout << "select " << code << ": " << item.name << " costs " << item.price << ", insert " << item.price - m.balance << " more\n";
return;
}
m.selected = &item;
m.move(&DISPENSING, "select " + code, "");
m.state->dispense(m);
}
void Dispensing::dispense(Machine& m) {
Item& item = *m.selected;
item.stock--;
string change = Machine::change(m.balance - item.price);
m.balance = 0;
bool empty = true;
for (auto& [code, i] : m.items) if (i.stock > 0) empty = false;
m.move(empty ? (State*)&OUT_OF_STOCK : &IDLE, "dispense", item.name + ", change " + change);
}
int main() {
Machine m;
m.items["A1"] = {"Chips", 15, 1}; m.items["A2"] = {"Juice", 22, 1};
m.state->insert(m, 10); m.state->select(m, "A1"); m.state->insert(m, 10); m.state->select(m, "A1");
m.state->insert(m, 10); m.state->insert(m, 10); m.state->insert(m, 10); m.state->select(m, "A2");
m.state->insert(m, 10);
}
import java.util.*;
public class Main {
static class Item {
final String name; final int price; int stock;
Item(String name, int price, int stock) { this.name = name; this.price = price; this.stock = stock; }
}
interface State {
String name();
void insert(Machine m, int coin);
default void select(Machine m, String code) { System.out.println("select " + code + ": ignored in " + name()); }
default void dispense(Machine m) {}
}
static class Idle implements State {
public String name() { return "Idle"; }
public void insert(Machine m, int coin) { m.balance += coin; m.move(new HasMoney(), "insert " + coin, "balance " + m.balance); }
}
static class HasMoney implements State {
public String name() { return "HasMoney"; }
public void insert(Machine m, int coin) { m.balance += coin; System.out.println("insert " + coin + ": balance " + m.balance); }
public void select(Machine m, String code) {
Item item = m.items.get(code);
if (item.stock == 0) { System.out.println("select " + code + ": " + item.name + " sold out"); return; }
if (m.balance < item.price) {
System.out.println("select " + code + ": " + item.name + " costs " + item.price + ", insert " + (item.price - m.balance) + " more");
return;
}
m.selected = item;
m.move(new Dispensing(), "select " + code, null);
m.state.dispense(m);
}
}
static class Dispensing implements State {
public String name() { return "Dispensing"; }
public void insert(Machine m, int coin) { System.out.println("insert " + coin + ": busy, returned " + coin); }
public void dispense(Machine m) {
Item item = m.selected;
item.stock--;
String change = Machine.change(m.balance - item.price);
m.balance = 0;
boolean empty = m.items.values().stream().allMatch(i -> i.stock == 0);
m.move(empty ? new OutOfStock() : new Idle(), "dispense", item.name + ", change " + change);
}
}
static class OutOfStock implements State {
public String name() { return "OutOfStock"; }
public void insert(Machine m, int coin) { System.out.println("insert " + coin + ": sold out, returned " + coin); }
}
static class Machine {
static final int[] COINS = {10, 5, 2, 1};
final Map<String, Item> items = new TreeMap<>();
State state = new Idle();
int balance = 0;
Item selected;
void move(State next, String event, String note) {
System.out.println(event + ": " + state.name() + " -> " + next.name() + (note == null ? "" : ", " + note));
state = next;
}
// Greedy: the largest coin that fits, again and again (optimal for 10/5/2/1).
static String change(int amount) {
List<String> coins = new ArrayList<>();
for (int c : COINS) while (amount >= c) { coins.add(String.valueOf(c)); amount -= c; }
return coins.isEmpty() ? "none" : String.join("+", coins);
}
}
public static void main(String[] args) {
Machine m = new Machine();
m.items.put("A1", new Item("Chips", 15, 1));
m.items.put("A2", new Item("Juice", 22, 1));
m.state.insert(m, 10); m.state.select(m, "A1"); m.state.insert(m, 10); m.state.select(m, "A1");
m.state.insert(m, 10); m.state.insert(m, 10); m.state.insert(m, 10); m.state.select(m, "A2");
m.state.insert(m, 10);
}
}
class Item:
def __init__(self, name, price, stock):
self.name, self.price, self.stock = name, price, stock
# Each state answers the events itself; the machine only holds the current one.
class State:
name = "State"
def insert(self, m, coin): ...
def select(self, m, code):
print(f"select {code}: ignored in {self.name}")
def dispense(self, m): ...
class Idle(State):
name = "Idle"
def insert(self, m, coin):
m.balance += coin
m.move(HasMoney(), f"insert {coin}", f"balance {m.balance}")
class HasMoney(State):
name = "HasMoney"
def insert(self, m, coin):
m.balance += coin
print(f"insert {coin}: balance {m.balance}")
def select(self, m, code):
item = m.items[code]
if item.stock == 0:
print(f"select {code}: {item.name} sold out")
return
if m.balance < item.price:
print(f"select {code}: {item.name} costs {item.price}, insert {item.price - m.balance} more")
return
m.selected = item
m.move(Dispensing(), f"select {code}", None)
m.state.dispense(m)
class Dispensing(State):
name = "Dispensing"
def insert(self, m, coin):
print(f"insert {coin}: busy, returned {coin}")
def dispense(self, m):
item = m.selected
item.stock -= 1
change = Machine.change(m.balance - item.price)
m.balance = 0
empty = all(i.stock == 0 for i in m.items.values())
m.move(OutOfStock() if empty else Idle(), "dispense", f"{item.name}, change {change}")
class OutOfStock(State):
name = "OutOfStock"
def insert(self, m, coin):
print(f"insert {coin}: sold out, returned {coin}")
class Machine:
def __init__(self):
self.items, self.state, self.balance, self.selected = {}, Idle(), 0, None
def move(self, nxt, event, note):
print(f"{event}: {self.state.name} -> {nxt.name}" + ("" if note is None else f", {note}"))
self.state = nxt
@staticmethod
def change(amount):
"""Greedy: the largest coin that fits, again and again (optimal for 10/5/2/1)."""
coins = []
for c in (10, 5, 2, 1):
while amount >= c:
coins.append(str(c))
amount -= c
return "+".join(coins) if coins else "none"
m = Machine()
m.items["A1"] = Item("Chips", 15, 1)
m.items["A2"] = Item("Juice", 22, 1)
m.state.insert(m, 10); m.state.select(m, "A1"); m.state.insert(m, 10); m.state.select(m, "A1")
m.state.insert(m, 10); m.state.insert(m, 10); m.state.insert(m, 10); m.state.select(m, "A2")
m.state.insert(m, 10)
class Item {
constructor(name, price, stock) { this.name = name; this.price = price; this.stock = stock; }
}
// Each state answers the events itself; the machine only holds the current one.
class State {
select(m, code) { console.log(`select ${code}: ignored in ${this.name}`); }
dispense(m) {}
}
class Idle extends State {
get name() { return "Idle"; }
insert(m, coin) { m.balance += coin; m.move(new HasMoney(), `insert ${coin}`, `balance ${m.balance}`); }
}
class HasMoney extends State {
get name() { return "HasMoney"; }
insert(m, coin) { m.balance += coin; console.log(`insert ${coin}: balance ${m.balance}`); }
select(m, code) {
const item = m.items.get(code);
if (item.stock === 0) { console.log(`select ${code}: ${item.name} sold out`); return; }
if (m.balance < item.price) {
console.log(`select ${code}: ${item.name} costs ${item.price}, insert ${item.price - m.balance} more`);
return;
}
m.selected = item;
m.move(new Dispensing(), `select ${code}`, null);
m.state.dispense(m);
}
}
class Dispensing extends State {
get name() { return "Dispensing"; }
insert(m, coin) { console.log(`insert ${coin}: busy, returned ${coin}`); }
dispense(m) {
const item = m.selected;
item.stock--;
const change = Machine.change(m.balance - item.price);
m.balance = 0;
const empty = [...m.items.values()].every((i) => i.stock === 0);
m.move(empty ? new OutOfStock() : new Idle(), "dispense", `${item.name}, change ${change}`);
}
}
class OutOfStock extends State {
get name() { return "OutOfStock"; }
insert(m, coin) { console.log(`insert ${coin}: sold out, returned ${coin}`); }
}
class Machine {
constructor() { this.items = new Map(); this.state = new Idle(); this.balance = 0; this.selected = null; }
move(next, event, note) {
console.log(`${event}: ${this.state.name} -> ${next.name}${note === null ? "" : `, ${note}`}`);
this.state = next;
}
// Greedy: the largest coin that fits, again and again (optimal for 10/5/2/1).
static change(amount) {
const coins = [];
for (const c of [10, 5, 2, 1]) while (amount >= c) { coins.push(c); amount -= c; }
return coins.length ? coins.join("+") : "none";
}
}
const m = new Machine();
m.items.set("A1", new Item("Chips", 15, 1));
m.items.set("A2", new Item("Juice", 22, 1));
m.state.insert(m, 10); m.state.select(m, "A1"); m.state.insert(m, 10); m.state.select(m, "A1");
m.state.insert(m, 10); m.state.insert(m, 10); m.state.insert(m, 10); m.state.select(m, "A2");
m.state.insert(m, 10);
Why is the State pattern used for a vending machine?
What a coin or a button does depends on what the machine is doing: a coin in Idle starts a sale, in OutOfStock it is returned. The State pattern puts each state's answers in its own class, replacing a growing chain of if-else on a status field.
What are the states of a vending machine?
Idle (waiting for money), HasMoney (collecting coins and waiting for a selection), Dispensing (dropping the item and the change) and OutOfStock (nothing left to sell). Real designs add Maintenance or a cancel path that refunds the balance.
How does a vending machine calculate change?
It subtracts the price from the balance and pays the rest with the largest coin that fits, repeatedly. That greedy rule gives the fewest coins for canonical sets like 10, 5, 2, 1, but not for every set; with limited coins in the machine, a dynamic-programming search is safer.
What happens if the customer cannot pay the full price?
HasMoney refuses the selection, tells the customer how much more is needed and stays in HasMoney. A cancel button returns the whole balance and moves the machine back to Idle.
How do you add a new state such as Maintenance?
Write one new class that implements the State interface and decide which existing states move into it. The machine and the other states' logic stay as they are, which is the open-closed principle at work.