Chapter 12
Problem 8: Design a Vending Machine
The setup
"Design a vending machine. User inserts coins/notes, selects an item, receives the item and change. Machine has inventory of multiple items with prices."
The vending machine is the canonical State pattern problem. It has explicit operational states (idle, has money, item selected, dispensing) with allowed transitions and behavior that varies per state.
Clarifying questions
| Question | Story answer |
|---|---|
| Payment types? | Coins (₹1, ₹2, ₹5, ₹10) and notes (₹10, ₹20, ₹50, ₹100). No card for v1. |
| Change-making? | Yes — machine must dispense correct change from its coin/note inventory. |
| What if insufficient change available? | Reject the transaction; refund inserted money. |
| Cancel mid-transaction? | Yes — user can cancel and get refund. |
| Refilling inventory? | Admin operation; out of scope for normal use cases. |
The State machine
States: IDLE — waiting for money or selection HAS_MONEY — money inserted; waiting for item selection ITEM_SELECTED — item chosen; verifying funds, preparing to dispense DISPENSING — item physically released RETURNING_CHANGE — releasing change Transitions: IDLE + insert_money → HAS_MONEY (or stays IDLE if invalid) HAS_MONEY + insert_more_money → HAS_MONEY (accumulate) HAS_MONEY + select_item → ITEM_SELECTED (if balance >= price) HAS_MONEY + cancel → IDLE (refund) ITEM_SELECTED + confirm → DISPENSING DISPENSING + done → RETURNING_CHANGE (if change > 0) or IDLE RETURNING_CHANGE + done → IDLE
interface MachineState {
void insertMoney(VendingMachine machine, Money money);
void selectItem(VendingMachine machine, String itemId);
void cancel(VendingMachine machine);
// Some states may make some methods no-ops or errors
}
class IdleState implements MachineState {
public void insertMoney(VendingMachine machine, Money money) {
machine.addInsertedAmount(money.value);
machine.transitionTo(new HasMoneyState());
}
public void selectItem(VendingMachine machine, String itemId) {
// No money inserted; reject
machine.displayMessage("Please insert money first");
}
public void cancel(VendingMachine machine) {} // noop
}
class HasMoneyState implements MachineState {
public void insertMoney(VendingMachine machine, Money money) {
machine.addInsertedAmount(money.value); // accumulate
}
public void selectItem(VendingMachine machine, String itemId) {
Item item = machine.getInventory().getItem(itemId);
if (item == null || item.quantity == 0) {
machine.displayMessage("Item unavailable");
return;
}
if (machine.insertedAmount.compareTo(item.price) < 0) {
machine.displayMessage("Insufficient funds; need "
+ item.price.subtract(machine.insertedAmount) + " more");
return;
}
// Check change availability
BigDecimal changeNeeded = machine.insertedAmount.subtract(item.price);
if (!machine.canMakeChange(changeNeeded)) {
machine.displayMessage("Exact change required");
machine.refund();
machine.transitionTo(new IdleState());
return;
}
machine.setSelectedItem(item);
machine.transitionTo(new ItemSelectedState());
}
public void cancel(VendingMachine machine) {
machine.refund();
machine.transitionTo(new IdleState());
}
}
class ItemSelectedState implements MachineState {
public void selectItem(VendingMachine machine, String itemId) {
// Same as confirm — proceed to dispense
machine.dispenseSelectedItem();
machine.transitionTo(new DispensingState());
}
// Cancel still refunds
}
class DispensingState implements MachineState {
// Most operations rejected during physical dispense
// After dispense, transition to RETURNING_CHANGE or IDLE
}
The VendingMachine class
class VendingMachine {
private MachineState state;
private Inventory inventory;
private CoinInventory coinInventory; // tracks coins/notes available for change
private BigDecimal insertedAmount;
private Item selectedItem;
public void insertMoney(Money money) { state.insertMoney(this, money); }
public void selectItem(String itemId) { state.selectItem(this, itemId); }
public void cancel() { state.cancel(this); }
void transitionTo(MachineState newState) { this.state = newState; }
void dispenseSelectedItem() {
BigDecimal change = insertedAmount.subtract(selectedItem.price);
// Physically dispense the item
inventory.removeItem(selectedItem.id);
coinInventory.addAmount(selectedItem.price); // money goes into machine
if (change.signum() > 0) {
List coinsForChange = coinInventory.makeChange(change);
// Physically dispense change
}
// Reset
insertedAmount = BigDecimal.ZERO;
selectedItem = null;
transitionTo(new IdleState());
}
}
Cross-examination round 1: change-making
Interviewer: I insert ₹100, item costs ₹40, change is ₹60. Machine has 5 × ₹10 = ₹50, 0 × ₹20, 2 × ₹50 = ₹100 in stock. Can it make change?
Tricky. Greedy says: try largest first → ₹50 (have 2); after one ₹50, need ₹10 more. Have 5 × ₹10. So: 1 × ₹50 + 1 × ₹10 = ₹60. Yes. The algorithm tries each denomination greedily. But: with non-canonical coin systems (rare in real currency), greedy may fail and DP is needed. For Indian or US currency, greedy works.
Follow-up: What if machine has 1 × ₹50 and 1 × ₹10 only (₹60 total)? Greedy works. What if 0 × ₹50, 1 × ₹20, 4 × ₹10? Greedy: try ₹50 (none), ₹20 (1 → ₹40 remaining? Wait need ₹60 total, used ₹20, need ₹40), then ₹10 × 4 = ₹40. Works: 1 × ₹20 + 4 × ₹10 = ₹60. Greedy correct for these denominations.
Takeaway
Vending machines are the State pattern's textbook example. Each state defines its allowed operations and transitions. Adding new operations or states is localised. This is far cleaner than a giant switch on a state enum inside one class.