The vending machine is the classic State pattern interview question. We already learned the State pattern earlier — now we’re applying it to a full design problem with real inventory, money handling, and change calculation. Interviewers love this because it tests whether we actually know when and how to use design patterns in practice.
The whole point: the machine behaves completely differently depending on what state it’s in. Insert money when idle? Cool, accepted. Insert money when already dispensing? Nope, wait. Same action, different behavior based on state.
Requirements
Functional:
- Machine holds multiple products, each with a price and quantity
- User inserts coins/money (we track the running total)
- User selects a product
- Machine dispenses product if enough money and in stock
- Machine returns change if overpaid
- Machine handles edge cases: out of stock, insufficient money, no change available
State Transitions:
- Idle -> (insert money) -> HasMoney
- HasMoney -> (select product, enough money) -> Dispensing
- HasMoney -> (cancel) -> Idle (refund money)
- Dispensing -> (product dispensed) -> Idle
Key Classes & Relationships
rejects select/dispense
allows product selection
returns change
goes back to Idle
Core Implementation
from abc import ABC, abstractmethod
# ---- Product & Inventory ----
class Product:
def __init__(self, code: str, name: str, price: float):
self.code = code
self.name = name
self.price = price
def __repr__(self):
return f"{self.name} (${self.price})"
class Inventory:
def __init__(self):
self.products = {} # code -> Product
self.quantities = {} # code -> int
def add_product(self, product: Product, quantity: int):
self.products[product.code] = product
self.quantities[product.code] = self.quantities.get(product.code, 0) + quantity
def get_product(self, code: str) -> Product:
return self.products.get(code)
def is_available(self, code: str) -> bool:
return self.quantities.get(code, 0) > 0
def dispense(self, code: str):
if self.is_available(code):
self.quantities[code] -= 1
# ---- State Interface ----
class State(ABC):
@abstractmethod
def insert_money(self, machine, amount: float) -> None:
pass
@abstractmethod
def select_product(self, machine, code: str) -> None:
pass
@abstractmethod
def dispense(self, machine) -> None:
pass
@abstractmethod
def cancel(self, machine) -> None:
pass
# ---- Concrete States ----
class IdleState(State):
def insert_money(self, machine, amount):
machine.balance += amount
print(f"Inserted ${amount}. Balance: ${machine.balance}")
machine.set_state(HasMoneyState())
def select_product(self, machine, code):
print("Insert money first!")
def dispense(self, machine):
print("Insert money and select a product first!")
def cancel(self, machine):
print("Nothing to cancel.")
class HasMoneyState(State):
def insert_money(self, machine, amount):
machine.balance += amount
print(f"Inserted ${amount}. Balance: ${machine.balance}")
def select_product(self, machine, code):
product = machine.inventory.get_product(code)
if not product:
print(f"Invalid code: {code}")
return
if not machine.inventory.is_available(code):
print(f"{product.name} is out of stock!")
return
if machine.balance < product.price:
print(f"Not enough money! {product.name} costs ${product.price}, "
f"balance is ${machine.balance}. Insert ${product.price - machine.balance} more.")
return
machine.selected_product = product
print(f"Selected: {product.name}. Dispensing...")
machine.set_state(DispensingState())
machine.dispense() # auto-trigger dispense
def dispense(self, machine):
print("Select a product first!")
def cancel(self, machine):
print(f"Cancelled. Refunding ${machine.balance}")
machine.balance = 0
machine.set_state(IdleState())
class DispensingState(State):
def insert_money(self, machine, amount):
print("Please wait, dispensing in progress.")
def select_product(self, machine, code):
print("Already dispensing, please wait.")
def dispense(self, machine):
product = machine.selected_product
machine.inventory.dispense(product.code)
change = machine.balance - product.price
print(f"Dispensed: {product.name}")
if change > 0:
print(f"Change returned: ${change}")
# Reset
machine.balance = 0
machine.selected_product = None
machine.set_state(IdleState())
def cancel(self, machine):
print("Cannot cancel during dispensing.")
# ---- Vending Machine ----
class VendingMachine:
def __init__(self):
self.inventory = Inventory()
self.balance = 0.0
self.selected_product = None
self._state = IdleState()
def set_state(self, state: State):
self._state = state
def insert_money(self, amount: float):
self._state.insert_money(self, amount)
def select_product(self, code: str):
self._state.select_product(self, code)
def dispense(self):
self._state.dispense(self)
def cancel(self):
self._state.cancel(self)
def show_products(self):
print("\n--- Available Products ---")
for code, product in self.inventory.products.items():
qty = self.inventory.quantities[code]
status = f"({qty} left)" if qty > 0 else "(OUT OF STOCK)"
print(f" [{code}] {product.name} - ${product.price} {status}")
print()
# ---- Usage ----
machine = VendingMachine()
# Stock the machine
machine.inventory.add_product(Product("A1", "Coca Cola", 1.50), 5)
machine.inventory.add_product(Product("A2", "Pepsi", 1.50), 3)
machine.inventory.add_product(Product("B1", "Lays Chips", 2.00), 2)
machine.inventory.add_product(Product("B2", "Snickers", 1.75), 0) # out of stock!
machine.show_products()
# Happy path
machine.insert_money(2.00) # Inserted $2.0. Balance: $2.0
machine.select_product("A1") # Selected: Coca Cola. Dispensing...
# Dispensed: Coca Cola
# Change returned: $0.5
# Not enough money
machine.insert_money(1.00) # Inserted $1.0. Balance: $1.0
machine.select_product("B1") # Not enough money! Lays costs $2.0...
# Add more money
machine.insert_money(1.00) # Inserted $1.0. Balance: $2.0
machine.select_product("B1") # Selected: Lays Chips. Dispensing...
# Out of stock
machine.insert_money(2.00)
machine.select_product("B2") # Snickers is out of stock!
machine.cancel() # Cancelled. Refunding $2.0
// ---- Product & Inventory ----
class Product {
constructor(code, name, price) {
this.code = code;
this.name = name;
this.price = price;
}
}
class Inventory {
constructor() {
this.products = new Map();
this.quantities = new Map();
}
addProduct(product, qty) {
this.products.set(product.code, product);
this.quantities.set(product.code, (this.quantities.get(product.code) || 0) + qty);
}
getProduct(code) { return this.products.get(code) || null; }
isAvailable(code) { return (this.quantities.get(code) || 0) > 0; }
dispense(code) {
if (this.isAvailable(code)) this.quantities.set(code, this.quantities.get(code) - 1);
}
}
// ---- States ----
class IdleState {
insertMoney(machine, amount) {
machine.balance += amount;
console.log(`Inserted $${amount}. Balance: $${machine.balance}`);
machine.setState(new HasMoneyState());
}
selectProduct(machine, code) { console.log("Insert money first!"); }
dispense(machine) { console.log("Insert money and select a product first!"); }
cancel(machine) { console.log("Nothing to cancel."); }
}
class HasMoneyState {
insertMoney(machine, amount) {
machine.balance += amount;
console.log(`Inserted $${amount}. Balance: $${machine.balance}`);
}
selectProduct(machine, code) {
const product = machine.inventory.getProduct(code);
if (!product) { console.log(`Invalid code: ${code}`); return; }
if (!machine.inventory.isAvailable(code)) {
console.log(`${product.name} is out of stock!`); return;
}
if (machine.balance < product.price) {
console.log(`Not enough! ${product.name} costs $${product.price}, ` +
`balance is $${machine.balance}`);
return;
}
machine.selectedProduct = product;
console.log(`Selected: ${product.name}. Dispensing...`);
machine.setState(new DispensingState());
machine.dispense();
}
dispense(machine) { console.log("Select a product first!"); }
cancel(machine) {
console.log(`Cancelled. Refunding $${machine.balance}`);
machine.balance = 0;
machine.setState(new IdleState());
}
}
class DispensingState {
insertMoney(machine, amount) { console.log("Please wait, dispensing."); }
selectProduct(machine, code) { console.log("Already dispensing, wait."); }
dispense(machine) {
const product = machine.selectedProduct;
machine.inventory.dispense(product.code);
const change = machine.balance - product.price;
console.log(`Dispensed: ${product.name}`);
if (change > 0) console.log(`Change: $${change}`);
machine.balance = 0;
machine.selectedProduct = null;
machine.setState(new IdleState());
}
cancel(machine) { console.log("Cannot cancel during dispensing."); }
}
// ---- Vending Machine ----
class VendingMachine {
constructor() {
this.inventory = new Inventory();
this.balance = 0;
this.selectedProduct = null;
this.state = new IdleState();
}
setState(state) { this.state = state; }
insertMoney(amount) { this.state.insertMoney(this, amount); }
selectProduct(code) { this.state.selectProduct(this, code); }
dispense() { this.state.dispense(this); }
cancel() { this.state.cancel(this); }
}
// Usage
const machine = new VendingMachine();
machine.inventory.addProduct(new Product("A1", "Coca Cola", 1.50), 5);
machine.inventory.addProduct(new Product("B1", "Lays Chips", 2.00), 2);
machine.insertMoney(2.00); // Inserted $2. Balance: $2
machine.selectProduct("A1"); // Selected: Coca Cola -> Dispensed + $0.5 change
import java.util.*;
class Product {
String code, name;
double price;
Product(String code, String name, double price) {
this.code = code; this.name = name; this.price = price;
}
}
class Inventory {
Map<String, Product> products = new HashMap<>();
Map<String, Integer> quantities = new HashMap<>();
void addProduct(Product p, int qty) {
products.put(p.code, p);
quantities.merge(p.code, qty, Integer::sum);
}
Product getProduct(String code) { return products.get(code); }
boolean isAvailable(String code) { return quantities.getOrDefault(code, 0) > 0; }
void dispense(String code) { quantities.computeIfPresent(code, (k, v) -> v - 1); }
}
// ---- State Interface ----
interface State {
void insertMoney(VendingMachine m, double amount);
void selectProduct(VendingMachine m, String code);
void dispense(VendingMachine m);
void cancel(VendingMachine m);
}
class IdleState implements State {
public void insertMoney(VendingMachine m, double amount) {
m.balance += amount;
System.out.printf("Inserted $%.2f. Balance: $%.2f%n", amount, m.balance);
m.setState(new HasMoneyState());
}
public void selectProduct(VendingMachine m, String code) {
System.out.println("Insert money first!");
}
public void dispense(VendingMachine m) {
System.out.println("Insert money and select a product first!");
}
public void cancel(VendingMachine m) {
System.out.println("Nothing to cancel.");
}
}
class HasMoneyState implements State {
public void insertMoney(VendingMachine m, double amount) {
m.balance += amount;
System.out.printf("Inserted $%.2f. Balance: $%.2f%n", amount, m.balance);
}
public void selectProduct(VendingMachine m, String code) {
Product product = m.inventory.getProduct(code);
if (product == null) { System.out.println("Invalid code!"); return; }
if (!m.inventory.isAvailable(code)) {
System.out.println(product.name + " is out of stock!"); return;
}
if (m.balance < product.price) {
System.out.printf("Not enough! %s costs $%.2f, balance $%.2f%n",
product.name, product.price, m.balance);
return;
}
m.selectedProduct = product;
System.out.println("Selected: " + product.name + ". Dispensing...");
m.setState(new DispensingState());
m.dispense();
}
public void dispense(VendingMachine m) { System.out.println("Select a product first!"); }
public void cancel(VendingMachine m) {
System.out.printf("Cancelled. Refunding $%.2f%n", m.balance);
m.balance = 0;
m.setState(new IdleState());
}
}
class DispensingState implements State {
public void insertMoney(VendingMachine m, double amount) {
System.out.println("Please wait, dispensing.");
}
public void selectProduct(VendingMachine m, String code) {
System.out.println("Already dispensing, wait.");
}
public void dispense(VendingMachine m) {
Product p = m.selectedProduct;
m.inventory.dispense(p.code);
double change = m.balance - p.price;
System.out.println("Dispensed: " + p.name);
if (change > 0) System.out.printf("Change: $%.2f%n", change);
m.balance = 0;
m.selectedProduct = null;
m.setState(new IdleState());
}
public void cancel(VendingMachine m) {
System.out.println("Cannot cancel during dispensing.");
}
}
class VendingMachine {
Inventory inventory = new Inventory();
double balance = 0;
Product selectedProduct = null;
private State state = new IdleState();
void setState(State s) { this.state = s; }
void insertMoney(double amount) { state.insertMoney(this, amount); }
void selectProduct(String code) { state.selectProduct(this, code); }
void dispense() { state.dispense(this); }
void cancel() { state.cancel(this); }
}
Why State Pattern Here?
Look at what each state does differently for the same actions:
| Action | IdleState | HasMoneyState | DispensingState |
|---|---|---|---|
insert_money | Accept, go to HasMoney | Accept, stay | Reject |
select_product | Reject | Validate & go to Dispensing | Reject |
dispense | Reject | Reject | Dispense, go to Idle |
cancel | No-op | Refund, go to Idle | Reject |
Without the State pattern, every method would have a big if-else checking which state we’re in. With it, each state class handles its own behavior. Adding a new state (like MaintenanceState) means adding one class — zero changes to existing code.
Design Patterns Used
State — The star of the show. Each state (Idle, HasMoney, Dispensing) is its own class. The VendingMachine delegates all actions to the current state. States transition themselves.
Composition — VendingMachine composes Inventory and State. Inventory composes Products. No deep inheritance trees.
Separation of concerns — Product knows about itself. Inventory manages stock. States manage behavior. VendingMachine is just the glue.
Extensions
- Card payment — Add a
PaymentMethodinterface withCashPaymentandCardPaymentimplementations. The HasMoneyState becomes more of a “HasPayment” state. - Admin refill — Add an
AdminServiceclass withrefill(code, quantity)andcollectMoney()methods. Only accessible with an admin key/PIN. - Multiple product selection — Let users add multiple items to a “cart” before dispensing. The HasMoneyState tracks a list of selected products instead of one.
- Coin denomination handling — Instead of tracking a simple balance, track individual coins inserted. Makes change calculation more realistic (can we even make change with the coins we have?).
- Display and notifications — Add a
Displayclass that shows current state, balance, and product info. Observer pattern to update display when state changes.
In simple language, the vending machine is the perfect warm-up for State pattern in interviews. The states are obvious, the transitions are clear, and the code practically writes itself once we identify the states. When the interviewer says “design a vending machine,” they’re really asking “show me you understand the State pattern.”