Vending Machine: the Canonical State Pattern
The machine's entire behavior swings on one axis — has it been paid enough yet — and modeling that axis as three state objects, Idle, HasMoney and Dispensing, instead of a switch on an enum, is the textbook first lesson in the State pattern.
2.1Problem Statement
Design the controller for a vending machine that holds products in slots and accepts coins or notes. It sounds trivial — a box that trades money for snacks — but the phrasing interviewers use is deliberately loaded, and mining it correctly is most of the exercise.
Functional requirements:
- The machine moves through states:
Idle → HasMoney → Dispensing → Idle. - A user selects a product, inserts money incrementally, and the machine dispenses the product and returns change — or refunds if the user cancels.
- Reject a selection if the product is out of stock, or reject dispensing if inserted money is insufficient.
- An admin can refill inventory and collect the accumulated cash.
Non-functional requirements / constraints:
- Behaviour must be driven by state objects, not a giant
if/switchon an enum. - Each state defines its own response to
insertCoin,selectProduct,dispense, andcancel.
Expected public interface, as an interviewer might hand it to you:
enum class Coin { ONE, FIVE, TEN, TWENTY };
class VendingMachine {
public:
void selectProduct(const std::string& code);
void insertCoin(Coin c);
Product dispense(); // valid only in Dispensing state
void cancel(); // refund all inserted money, return to Idle
// admin
void refill(const std::string& code, int qty);
int collectCash();
};
class State { // base class for IdleState, HasMoneyState, DispensingState
public:
virtual void selectProduct(VendingMachine&, const std::string&) = 0;
virtual void insertCoin(VendingMachine&, Coin) = 0;
virtual Product dispense(VendingMachine&) = 0;
virtual void cancel(VendingMachine&) = 0;
virtual ~State() = default;
};
What the interviewer is really testing:
- A proper State pattern: concrete state classes, with transitions owned by the states themselves rather than by the machine.
- That illegal operations in a given state are handled by that state — for example, calling
dispense()while stillIdlemust be rejected cleanly, not crash or silently no-op.
Follow-up questions to expect:
- Change-making: return optimal change from the available denominations. This sneaks in a small greedy/DP algorithm, and an interviewer will push on "what if you can't make exact change?"
- Concurrent users competing for the same slot — two people buying the last unit at once.
- Multiple payment methods (card, UPI) behind a payment abstraction, instead of coins only.
2.2How to Approach
Read the problem the way an interviewer wrote it, and mine the hard constraints first — they decide the design before you write a single class.
"Behaviour must be driven by state objects, not a giant if/switch on an enum" is not a style preference, it is the answer key. A switch (currentState) solution is a fail here even if it works, because the whole exercise is the State pattern. And "each state defines its own response to insertCoin, selectProduct, dispense, cancel" tells you the shape of the interface: every state implements the same four operations, and the difference between states is purely how each one reacts.
Next, find the nouns in the problem and let them become your classes:
| Class | Owns | Why it exists |
|---|---|---|
| Coin (enum) | a denomination value | the unit of money |
| Product | name, price, quantity | what's for sale in a slot |
| VendingState (interface) | the four operations | one reaction-set per machine mode |
| IdleState / HasMoneyState / DispenseState | transition rules for that mode | the actual behaviour, one class per mode |
| VendingMachine (the Context) | current state, balance, inventory | delegates every call to its current state |
That table is the whole State pattern in miniature: one Context (the machine), one State interface, and N concrete states. Once you can point at those three roles in any "machine with modes" problem, the class list writes itself.
Why State beats a switch on an enum. A switch-based version needs one case per (state × operation) pair living together in one function, repeated across four functions — twelve branches for three states and four operations, all coupled in the same file. Add a fourth state (say, OutOfService) and you must find and edit every one of those four switches correctly, in every place, or risk a silent gap. The State pattern inverts this: each state class owns its own row of the truth table in one place, so adding a state means writing one new class and touching zero existing code. That's the open/closed principle earning its keep, not just a buzzword.
Identify the states. The problem already names them: Idle (nothing selected, no money in), HasMoney (a product is chosen and the machine is accepting coins toward its price), and Dispensing (paid in full, ready to hand over the product and any change). Each is a class implementing the same abstract interface:
class VendingState{
public:
virtual void insertMoney(VendingMachine* m , int amount) = 0;
virtual void selectProduct(VendingMachine* m, int code) = 0;
virtual void dispenseProduct(VendingMachine* m) = 0;
virtual void refund(VendingMachine* m) = 0;
virtual ~VendingState() = default;
};
The Context, VendingMachine, holds the current state pointer plus all the real data (balance, inventory, selected code), and its public API is pure delegation — one-liners that forward to whichever state is current:
void insertMoney(int amount){ currentState->insertMoney(this, amount); }
If you find yourself writing an if inside one of the machine's public methods, the logic has leaked out of the states and back into the context — that's the tell that something drifted from the pattern.
- Context
- The object that holds the current state and forwards every call to it. Here,
VendingMachine. It owns the data; the states own the behaviour. - Delegation
- A method whose entire body is "ask someone else to do this." The Context's public methods should be almost nothing but delegation.
- Open/Closed Principle
- Code should be open to extension, closed to modification. Adding
OutOfServiceStateas a new class satisfies this; adding a new case to an existing switch violates it.
setState(...) on the machine. That keeps the machine dumb and each state cohesive — it knows its own exits without needing to know the whole map.Finally, treat each state's four methods as a truth table: for each operation, is it valid in this state? If yes, do the work and transition; if no, reject cleanly (the reference solution below prints a message and stays put, rather than throwing, to keep the demo simple — a production version might throw or return a status code instead). Fill every cell of that table and there is no undefined behaviour left to find.
2.3Whole Solution
The complete, compiling reference implementation. Three concrete states (IdleState, HasMoneyState, DispenseState) implement the VendingState interface; VendingMachine is the context, owning the three state instances and delegating every public call to whichever one is current. main() runs one full happy-path cycle plus one rejected call, to prove both the transitions and the rejection logic.
#include <bits/stdc++.h>
using namespace std;
class VendingMachine; // fundamental cycle: state <-> machine
// ---------- Data ----------
struct Product {
string name;
int price;
int qty;
};
// ---------- State interface ----------
class VendingState{
public:
virtual void insertMoney(VendingMachine* m , int amount) = 0;
virtual void selectProduct(VendingMachine* m, int code) = 0;
virtual void dispenseProduct(VendingMachine* m) = 0;
virtual void refund(VendingMachine* m) = 0;
virtual ~VendingState() = default;
};
// ---------- Context ----------
class VendingMachine{
private:
VendingState* currentState;
VendingState* idle;
VendingState* hasMoney;
VendingState* dispense;
int balance = 0;
int selectedCode = -1;
map<int, Product> inventory; // code -> product
public:
VendingMachine(); // body at bottom (needs concrete states)
~VendingMachine();
void setState(VendingState* s){ currentState = s; }
VendingState* getIdle(){ return idle; }
VendingState* getHasMoney(){ return hasMoney; }
VendingState* getDispense(){ return dispense; }
// money
void addBalance(int amt){ balance += amt; }
int getBalance(){ return balance; }
void resetBalance(){ balance = 0; }
// inventory
void addProduct(int code, const Product& p){ inventory[code] = p; }
bool hasStock(int code){ return inventory.count(code) && inventory[code].qty > 0; }
int getPrice(int code){ return inventory[code].price; }
string getName(int code){ return inventory[code].name; }
void reduceStock(int code){ inventory[code].qty--; }
// selection
void setSelected(int code){ selectedCode = code; }
int getSelected(){ return selectedCode; }
// public API -> delegates to current state
void insertMoney(int amount){ currentState->insertMoney(this, amount); }
void selectProduct(int code){ currentState->selectProduct(this, code); }
void dispenseProduct(){ currentState->dispenseProduct(this); }
void refund(){ currentState->refund(this); }
};
// ---------- Concrete states (all inline now: no sibling class ever named) ----------
class IdleState : public VendingState{
public:
void insertMoney(VendingMachine* m, int amount) override {
m->addBalance(amount);
cout << "Money in: " << amount << " | balance=" << m->getBalance() << endl;
m->setState(m->getHasMoney());
}
void selectProduct(VendingMachine* m, int code) override {
cout << "Insert Money First" << endl;
}
void dispenseProduct(VendingMachine* m) override {
cout << "Insert Money First" << endl;
}
void refund(VendingMachine* m) override {
cout << "Nothing to refund" << endl;
}
};
class HasMoneyState : public VendingState{
public:
void insertMoney(VendingMachine* m, int amount) override {
m->addBalance(amount);
cout << "Money in: " << amount << " | balance=" << m->getBalance() << endl;
}
void selectProduct(VendingMachine* m, int code) override {
if(!m->hasStock(code)){
cout << "Out of stock for code " << code << endl;
return;
}
if(m->getBalance() < m->getPrice(code)){
cout << "Insufficient money. Need " << m->getPrice(code)
<< ", have " << m->getBalance() << endl;
return;
}
m->setSelected(code);
cout << "Selected: " << m->getName(code) << endl;
m->setState(m->getDispense());
}
void dispenseProduct(VendingMachine* m) override {
cout << "Select a product first" << endl;
}
void refund(VendingMachine* m) override {
cout << "Refund: " << m->getBalance() << endl;
m->resetBalance();
m->setState(m->getIdle());
}
};
class DispenseState : public VendingState{
public:
void insertMoney(VendingMachine* m, int amount) override {
cout << "Dispensing in progress, cannot add money" << endl;
}
void selectProduct(VendingMachine* m, int code) override {
cout << "Already dispensing" << endl;
}
void dispenseProduct(VendingMachine* m) override {
int code = m->getSelected();
int change = m->getBalance() - m->getPrice(code);
m->reduceStock(code);
cout << "Dispensed: " << m->getName(code) << endl;
if(change > 0) cout << "Change returned: " << change << endl;
m->resetBalance();
m->setSelected(-1);
m->setState(m->getIdle());
}
void refund(VendingMachine* m) override { // changed mind before collecting
cout << "Refund: " << m->getBalance() << endl;
m->resetBalance();
m->setSelected(-1);
m->setState(m->getIdle());
}
};
// ---------- ctor/dtor: only place that knows every concrete state ----------
VendingMachine::VendingMachine(){
idle = new IdleState();
hasMoney = new HasMoneyState();
dispense = new DispenseState();
currentState = idle;
}
VendingMachine::~VendingMachine(){
delete idle;
delete hasMoney;
delete dispense;
}
// ---------- demo ----------
int main(){
VendingMachine m;
m.addProduct(1, {"Coke", 25, 2});
m.addProduct(2, {"Water", 15, 5});
m.selectProduct(1); // reject: insert money first
m.insertMoney(10); // idle -> hasMoney
m.insertMoney(20); // top up, balance=30
m.selectProduct(1); // ok -> dispense
m.dispenseProduct(); // dispense Coke, change 5, -> idle
return 0;
}
How would you make change from a limited set of denominations, and what if exact change isn't possible?
With an unlimited canonical coin set like {1, 5, 10, 20}, greedy from the largest denomination down always works, and always terminates because a 1-unit coin exists: for each coin large to small: take amount/coin of them, reduce amount. The moment coins are limited in supply, greedy can fail even though a valid combination exists, so you track per-denomination counts remaining and fall back to a coin-change-style DP (minimum coins, or "is it even possible") when greedy runs dry. If no combination reaches the exact amount, the honest answer is to refuse the dispense and refund instead of shortchanging the customer — say that fallback out loud, because naming it is what separates a considered answer from a lucky one.
Two users try to buy the last unit of the same product at the same time — how do you handle it?
The State pattern itself doesn't change; concurrency is a property of the one shared mutation, hasStock → reduceStock on the inventory. Guard that check-then-act sequence with a mutex (or a compare-and-swap on the quantity) so the two requests can't both read "1 left" and both decrement. The loser of the race should see the same "out of stock" rejection an ordinary late buyer would get — it's a correctness bug, not a new code path, so it belongs in the inventory layer, not in the state classes.
How do you add card or UPI payment without touching the State pattern?
Introduce a PaymentStrategy interface with something like authorize(amount), and inject one into the machine. Coin insertion becomes the "cash" implementation of that strategy; card and UPI become sibling implementations. This is the Strategy pattern sitting beside the State pattern: how money arrives is orthogonal to what mode the machine is in, so the state classes keep calling the same abstraction regardless of which concrete payment method is plugged in underneath. The same separation is why adding a fourth state, like OutOfServiceState, is just one more class and no edits to existing ones — the pattern was built to absorb exactly this kind of change.