The analogy: a physical state dial
Think of a vending machine like a mechanical music box with a circular dial. The dial only turns in one direction, pointing to different notches: Idle (waiting for coins), HasMoney (coins inserted, waiting for selection), Dispensing (releasing the soda), and SoldOut (no soda left).
A customer pushing the "Dispense" button while the dial points to Idle does nothing — the physical gear is locked. The gear only unlocks when the dial turns to HasMoney and a selection is pressed.
If we model this machine using simple switches and flags in code, we end up with an unmanageable mess of interlocking gears that easily jams. By using the State pattern, we give each notch on the dial its own independent class that knows exactly which moves are legal, creating a clean, modular, and failure-proof machine.
The ask
A machine that accepts money, lets a user select a product, dispenses it with correct change, and survives the messy reality: insufficient funds, sold-out slots, cancel/refund mid-transaction, and an admin who restocks. Forty-five minutes, clean OOP, working code.
It's a state-machine in disguise — interviewers use it to see whether you model behaviour with polymorphism or drown in conditionals. SDE-1: correct classes and a working happy path. SDE-2: the State pattern, clean change-making, and edge cases. SDE-3: you extend it without touching existing states (Strategy for payment), reason about concurrency and partial-failure, and name the trade-offs. Asked at Amazon, Microsoft, Atlassian, Walmart Global Tech, Uber, Flipkart; siblings are the coffee machine, ATM (see atm) and the elevator (see elevator).
The insight — model states, not a flag
The naive design is a status enum and a giant switch inside every method
(insertMoney, selectProduct, dispense, cancel). With S states and A actions
you get S × A branches scattered across the class, and every new state edits every
method — the change that breaks three others.
The State pattern flips it: each state is a class that knows only its own legal
transitions. "Select a product before paying" is rejected in IdleState.selectProduct,
in one place, and the context just delegates.
Think it through like the interview
PROBLEMAccept money, select a product, dispense with correct change. Handle insufficient funds, sold-out, cancel/refund, and restock — with clean, extensible OOP.
- 1
Spec → a (state × action) matrix
“What are the states, and what actions can a user attempt in each?”
- 2
Spot where the naive design rots
“If state is just an enum, where does the logic for these transitions live?”
unlocks after the stage above - 3
State pattern: one class per state
“How do I make illegal actions impossible to forget?”
unlocks after the stage above - 4
Money & change — and where greedy breaks
“How do I return change, and is greedy always correct?”
unlocks after the stage above - 5
Edge cases & partial failure
“Money taken, then the motor jams mid-dispense. Now what?”
unlocks after the stage above
Implementation
interface State {
insertMoney(cents: number): void;
selectProduct(code: string): void;
dispense(): void;
cancel(): void;
}
type Slot = { product: string; priceCents: number; qty: number };
class VendingMachine {
readonly idle = new IdleState(this);
readonly hasMoney = new HasMoneyState(this);
readonly dispensing = new DispensingState(this);
state: State = this.idle;
balanceCents = 0;
selected: string | null = null;
constructor(private inventory: Map<string, Slot>) {}
setState(s: State) { this.state = s; }
slot(code: string) { return this.inventory.get(code); }
inStock(code: string) { return (this.slot(code)?.qty ?? 0) > 0; }
insertMoney(c: number) { this.state.insertMoney(c); }
selectProduct(code: string) { this.state.selectProduct(code); }
dispense() { this.state.dispense(); }
cancel() { this.state.cancel(); }
refund() { const r = this.balanceCents; this.balanceCents = 0; return r; }
}
class IdleState implements State {
constructor(private m: VendingMachine) {}
insertMoney(c: number) { this.m.balanceCents += c; this.m.setState(this.m.hasMoney); }
selectProduct() { throw new Error("Insert money first"); }
dispense() { throw new Error("Insert money and select a product first"); }
cancel() { /* nothing to refund */ }
}
class HasMoneyState implements State {
constructor(private m: VendingMachine) {}
insertMoney(c: number) { this.m.balanceCents += c; }
selectProduct(code: string) {
const slot = this.m.slot(code);
if (!slot || slot.qty === 0) throw new Error("Sold out");
if (this.m.balanceCents < slot.priceCents)
throw new Error(`Need ${slot.priceCents - this.m.balanceCents}¢ more`);
this.m.selected = code;
this.m.setState(this.m.dispensing);
this.m.dispense();
}
dispense() { throw new Error("Select a product first"); }
cancel() { console.log(`Refunded ${this.m.refund()}¢`); this.m.setState(this.m.idle); }
}
class DispensingState implements State {
constructor(private m: VendingMachine) {}
insertMoney() { throw new Error("Dispensing — please wait"); }
selectProduct() { throw new Error("Dispensing — please wait"); }
dispense() {
const code = this.m.selected!;
const slot = this.m.slot(code)!;
slot.qty -= 1;
const change = this.m.balanceCents - slot.priceCents;
this.m.balanceCents = 0;
this.m.selected = null;
this.m.setState(this.m.idle);
console.log(`Dispensed ${slot.product}, change ${change}¢`);
}
cancel() { throw new Error("Too late to cancel — already dispensing"); }
}
1. Python
from abc import ABC, abstractmethod
import threading
class State(ABC):
@abstractmethod
def insert_money(self, amount: int): pass
@abstractmethod
def select_product(self, code: str): pass
@abstractmethod
def dispense(self): pass
@abstractmethod
def cancel(self): pass
class VendingMachine:
def __init__(self):
self.idle_state = IdleState(self)
self.has_money_state = HasMoneyState(self)
self.dispensing_state = DispensingState(self)
self.state: State = self.idle_state
self.balance = 0
self.selected_product_code: str = ""
self.inventory = {"cola": {"price": 150, "qty": 5}}
self._lock = threading.Lock()
def set_state(self, state: State):
self.state = state
def insert_money(self, amount: int):
with self._lock: self.state.insert_money(amount)
def select_product(self, code: str):
with self._lock: self.state.select_product(code)
def dispense(self):
with self._lock: self.state.dispense()
def cancel(self):
with self._lock: self.state.cancel()
class IdleState(State):
def __init__(self, machine: VendingMachine): self.m = machine
def insert_money(self, amount: int):
self.m.balance += amount
self.m.set_state(self.m.has_money_state)
def select_product(self, code: str): raise Exception("Insert money first")
def dispense(self): raise Exception("Insert money first")
def cancel(self): pass
class HasMoneyState(State):
def __init__(self, machine: VendingMachine): self.m = machine
def insert_money(self, amount: int): self.m.balance += amount
def select_product(self, code: str):
item = self.m.inventory.get(code)
if not item or item["qty"] == 0: raise Exception("Sold out")
if self.m.balance < item["price"]: raise Exception("Insufficient funds")
self.m.selected_product_code = code
self.m.set_state(self.m.dispensing_state)
def dispense(self): raise Exception("Select a product first")
def cancel(self):
refund = self.m.balance
self.m.balance = 0
self.m.set_state(self.m.idle_state)
print(f"Refunded {refund} cents")
class DispensingState(State):
def __init__(self, machine: VendingMachine): self.m = machine
def insert_money(self, amount: int): raise Exception("Dispensing in progress")
def select_product(self, code: str): raise Exception("Dispensing in progress")
def dispense(self):
code = self.m.selected_product_code
item = self.m.inventory[code]
item["qty"] -= 1
change = self.m.balance - item["price"]
self.m.balance = 0
self.m.selected_product_code = ""
self.m.set_state(self.m.idle_state)
print(f"Dispensed {code}, returned {change} cents change")
def cancel(self): raise Exception("Too late to cancel")
2. Java
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
interface State {
void insertMoney(VendingMachine m, int amount);
void selectProduct(VendingMachine m, String code);
void dispense(VendingMachine m);
void cancel(VendingMachine m);
}
class VendingMachine {
public final State idle = new IdleState();
public final State hasMoney = new HasMoneyState();
public final State dispensing = new DispensingState();
private State currentState = idle;
private int balance = 0;
private String selectedCode = null;
private final Map<String, Integer> inventory = new HashMap<>();
private final Map<String, Integer> prices = new HashMap<>();
private final ReentrantLock lock = new ReentrantLock();
public VendingMachine() {
inventory.put("cola", 5);
prices.put("cola", 150);
}
public void setState(State s) { this.currentState = s; }
public State getState() { return currentState; }
public void addBalance(int amount) { this.balance += amount; }
public int getBalance() { return balance; }
public void setBalance(int b) { this.balance = b; }
public String getSelectedCode() { return selectedCode; }
public void setSelectedCode(String code) { this.selectedCode = code; }
public int getPrice(String code) { return prices.getOrDefault(code, 0); }
public int getQty(String code) { return inventory.getOrDefault(code, 0); }
public void decrementQty(String code) { inventory.put(code, inventory.get(code) - 1); }
public void insertMoney(int amount) {
lock.lock();
try { currentState.insertMoney(this, amount); } finally { lock.unlock(); }
}
public void selectProduct(String code) {
lock.lock();
try { currentState.selectProduct(this, code); } finally { lock.unlock(); }
}
public void dispense() {
lock.lock();
try { currentState.dispense(this); } finally { lock.unlock(); }
}
public void cancel() {
lock.lock();
try { currentState.cancel(this); } finally { lock.unlock(); }
}
}
class IdleState implements State {
public void insertMoney(VendingMachine m, int amount) {
m.addBalance(amount);
m.setState(m.hasMoney);
}
public void selectProduct(VendingMachine m, String code) { throw new IllegalStateException("Insert money first"); }
public void dispense(VendingMachine m) { throw new IllegalStateException("Insert money first"); }
public void cancel(VendingMachine m) {}
}
class HasMoneyState implements State {
public void insertMoney(VendingMachine m, int amount) { m.addBalance(amount); }
public void selectProduct(VendingMachine m, String code) {
if (m.getQty(code) <= 0) throw new IllegalStateException("Sold out");
if (m.getBalance() < m.getPrice(code)) throw new IllegalStateException("Insufficient funds");
m.setSelectedCode(code);
m.setState(m.dispensing);
}
public void dispense(VendingMachine m) { throw new IllegalStateException("Select product first"); }
public void cancel(VendingMachine m) {
int refund = m.getBalance();
m.setBalance(0);
m.setState(m.idle);
System.out.println("Refunded " + refund + " cents");
}
}
class DispensingState implements State {
public void insertMoney(VendingMachine m, int amount) { throw new IllegalStateException("Dispensing in progress"); }
public void selectProduct(VendingMachine m, String code) { throw new IllegalStateException("Dispensing in progress"); }
public void dispense(VendingMachine m) {
String code = m.getSelectedCode();
m.decrementQty(code);
int change = m.getBalance() - m.getPrice(code);
m.setBalance(0);
m.setSelectedCode(null);
m.setState(m.idle);
System.out.println("Dispensed " + code + ", returned " + change + " cents change");
}
public void cancel(VendingMachine m) { throw new IllegalStateException("Too late to cancel"); }
}
3. C++
#include <iostream>
#include <string>
#include <unordered_map>
#include <memory>
#include <mutex>
#include <stdexcept>
class VendingMachine;
class State {
public:
virtual ~State() = default;
virtual void insertMoney(VendingMachine& m, int amount) = 0;
virtual void selectProduct(VendingMachine& m, const std::string& code) = 0;
virtual void dispense(VendingMachine& m) = 0;
virtual void cancel(VendingMachine& m) = 0;
};
class VendingMachine {
private:
std::shared_ptr<State> state;
int balance = 0;
std::string selected_code = "";
std::unordered_map<std::string, int> inventory;
std::unordered_map<std::string, int> prices;
std::mutex mtx;
public:
std::shared_ptr<State> idle;
std::shared_ptr<State> has_money;
std::shared_ptr<State> dispensing;
VendingMachine();
void setState(std::shared_ptr<State> s) { state = s; }
void addBalance(int amount) { balance += amount; }
int getBalance() const { return balance; }
void setBalance(int b) { balance = b; }
std::string getSelectedCode() const { return selected_code; }
void setSelectedCode(const std::string& code) { selected_code = code; }
int getQty(const std::string& code) { return inventory[code]; }
void decrementQty(const std::string& code) { inventory[code]--; }
int getPrice(const std::string& code) { return prices[code]; }
void insertMoney(int amount);
void selectProduct(const std::string& code);
void dispense();
void cancel();
};
class IdleState : public State {
public:
void insertMoney(VendingMachine& m, int amount) override;
void selectProduct(VendingMachine& m, const std::string& code) override { throw std::runtime_error("Insert money first"); }
void dispense(VendingMachine& m) override { throw std::runtime_error("Insert money first"); }
void cancel(VendingMachine& m) override {}
};
class HasMoneyState : public State {
public:
void insertMoney(VendingMachine& m, int amount) override { m.addBalance(amount); }
void selectProduct(VendingMachine& m, const std::string& code) override;
void dispense(VendingMachine& m) override { throw std::runtime_error("Select product first"); }
void cancel(VendingMachine& m) override;
};
class DispensingState : public State {
public:
void insertMoney(VendingMachine& m, int amount) override { throw std::runtime_error("Dispensing in progress"); }
void selectProduct(VendingMachine& m, const std::string& code) override { throw std::runtime_error("Dispensing in progress"); }
void dispense(VendingMachine& m) override;
void cancel(VendingMachine& m) override { throw std::runtime_error("Too late to cancel"); }
};
VendingMachine::VendingMachine() {
idle = std::make_shared<IdleState>();
has_money = std::make_shared<HasMoneyState>();
dispensing = std::make_shared<DispensingState>();
state = idle;
inventory["cola"] = 5;
prices["cola"] = 150;
}
void VendingMachine::insertMoney(int amount) { std::lock_guard<std::mutex> lock(mtx); state->insertMoney(*this, amount); }
void VendingMachine::selectProduct(const std::string& code) { std::lock_guard<std::mutex> lock(mtx); state->selectProduct(*this, code); }
void VendingMachine::dispense() { std::lock_guard<std::mutex> lock(mtx); state->dispense(*this); }
void VendingMachine::cancel() { std::lock_guard<std::mutex> lock(mtx); state->cancel(*this); }
void IdleState::insertMoney(VendingMachine& m, int amount) {
m.addBalance(amount);
m.setState(m.has_money);
}
void HasMoneyState::selectProduct(VendingMachine& m, const std::string& code) {
if (m.getQty(code) <= 0) throw std::runtime_error("Sold out");
if (m.getBalance() < m.getPrice(code)) throw std::runtime_error("Insufficient funds");
m.setSelectedCode(code);
m.setState(m.dispensing);
}
void HasMoneyState::cancel(VendingMachine& m) {
int refund = m.getBalance();
m.setBalance(0);
m.setState(m.idle);
std::cout << "Refunded " << refund << " cents\n";
}
void DispensingState::dispense(VendingMachine& m) {
std::string code = m.getSelectedCode();
m.decrementQty(code);
int change = m.getBalance() - m.getPrice(code);
m.setBalance(0);
m.setSelectedCode("");
m.setState(m.idle);
std::cout << "Dispensed " << code << ", returned " << change << " cents change\n";
}
Each state owns its legal transitions, so an illegal action is rejected in exactly one
place — no switch smeared across four methods. Adding a SoldOut or CardAccepted
state is a new class, not an edit to every existing method (open/closed principle).
Say this trade-off out loud; it's the difference between "wrote code" and "designed."
Check yourself
1. In the Vending Machine implementation, how does the State pattern satisfy the Open/Closed Principle (OCP)?
2. Why is the dispense() method call often triggered automatically from selectProduct() once payment is confirmed, rather than waiting for an external client call?
3. What is a major trade-off of the State pattern compared to a status enum switch?
Complexity & follow-ups
- Change-making: greedy is O(coins) and optimal for canonical sets; for arbitrary denominations it's the coin-change DP — know both and when each applies.
- Payment methods (cash, card, UPI): wrap behind a
PaymentStrategyinterface so the state machine never changes when you add a rail — Strategy + State compose cleanly. - Concurrency: a physical machine is single-user, but admin restock can race a
purchase — guard each
Slotmutation with a lock; model the dispense as reserve → dispense → commit/rollback so a jam never charges the customer. - Observability: emit events (sale, sold-out, jam) so you can answer "which slots sell out fastest?" — the product-thinking follow-up.
Design drills
Whiteboard each one out loud for 5–10 minutes before you reveal what a strong answer covers — the gap between your sketch and the checklist is your study list. Progress is saved on this device.
Add card and UPI payments without touching any State class.
An admin restocks a slot while a customer is mid-purchase on it. Keep it consistent.
The motor jams after the customer paid but before the item drops. Design the recovery.
Support arbitrary coin denominations where greedy change is wrong.