Vending Machine
State pattern turns a mess of if-else chains into clean, self-contained state objects. Each state knows what it can do and when to hand off to the next one. The machine just delegates.
Key Abstractions
Context object that delegates all behavior to the current state
Interface with methods for each user action: insertMoney, selectProduct, dispense
Manages product stock, pricing, and availability checks
Handles coin/note acceptance and change calculation using greedy algorithm
Class Diagram
The Key Insight
A vending machine at any point in time is doing exactly one thing: waiting for money, waiting for a selection, or dispensing a product. These are discrete states, and the actions a user can perform depend entirely on which state the machine is in. Inserting money while dispensing? Invalid. Selecting a product with no balance? Rejected.
You could model this with a big if currentState == ... block. It works for three states. It falls apart when you add maintenance mode, refund processing, or admin restocking. Each new state means updating every conditional branch in the system.
The State pattern fixes this by giving each state its own class. Each class only handles the actions that make sense for that state. The machine itself holds a reference to the current state and delegates everything. State transitions happen inside the state objects, so the machine never has to figure out what state it should be in next.
Requirements
Functional
- Accept coins and notes, track running balance
- Display available products with prices
- Dispense selected product if balance is sufficient
- Return correct change after purchase
- Allow users to cancel and receive full refund
- Track inventory and reject selections for out-of-stock items
Non-Functional
- State transitions must be atomic: no half-dispensed, half-charged scenarios
- Change calculation should handle any valid denomination set
- Adding new states (maintenance, restocking) should not require modifying existing state classes
Design Decisions
Why State pattern over a switch on an enum?
With a switch statement, every method in VendingMachine needs to check the current state. Adding a maintenance state means editing insertMoney, selectProduct, dispense, and cancel. With the State pattern, you write one new MaintenanceState class. Existing states remain untouched. Open-closed principle in practice.
Why a greedy algorithm for change?
Standard coin denominations (1, 5, 10, 25, 50, 100) are canonical, meaning greedy always produces the optimal solution. You never need dynamic programming here. Just iterate from largest to smallest denomination and subtract. Simple and correct.
Why separate Inventory from the state machine?
Inventory management (adding stock, checking availability, tracking quantities) is a concern independent of whether the machine is idle or dispensing. Mixing them means your IdleState needs to know about stock counts. Keeping Inventory as its own class means the state objects only deal with transitions and the inventory handles its own bookkeeping.
Why is currentBalance on the machine, not in the state?
Balance persists across state transitions. If a user inserts money (IdleState to HasMoneyState) and then adds more (still HasMoneyState), the running total needs to survive. The machine is the right owner because it outlives any individual state.
Interview Follow-ups
- "How would you add a maintenance mode?" Create a MaintenanceState that rejects all user actions and only responds to admin unlock commands. The machine transitions to it from any state when an admin triggers maintenance.
- "What if denominations are not canonical?" Replace the greedy algorithm with dynamic programming for change calculation. The PaymentProcessor interface stays the same, only the implementation changes.
- "How do you handle concurrent users?" Vending machines are inherently single-user, but if you model a digital version, synchronize state transitions with a lock. The State pattern makes this easier because each transition is a single method call.
- "How would you add payment card support?" Add a CardPaymentProcessor alongside the coin-based one. The Strategy pattern on payment processing lets you support both without changing the state machine.
Code Implementation
1 from __future__ import annotations
2 from abc import ABC, abstractmethod
3
4
5 class Product:
6 def __init__(self, code: str, name: str, price: int, quantity: int):
7 self.code = code
8 self.name = name
9 self.price = price
10 self.quantity = quantity
11
12 def __repr__(self) -> str:
13 return f"Product({self.name}, price={self.price}, qty={self.quantity})"
14
15
16 class Inventory:
17 """Manages product stock and pricing."""
18
19 def __init__(self):
20 self._products: dict[str, Product] = {}
21
22 def add_product(self, product: Product) -> None:
23 self._products[product.code] = product
24
25 def is_available(self, code: str) -> bool:
26 p = self._products.get(code)
27 return p is not None and p.quantity > 0
28
29 def get_price(self, code: str) -> int | None:
30 p = self._products.get(code)
31 return p.price if p else None
32
33 def get_product_name(self, code: str) -> str:
34 p = self._products.get(code)
35 return p.name if p else "Unknown"
36
37 def dispense(self, code: str) -> Product:
38 p = self._products[code]
39 if p.quantity <= 0:
40 raise ValueError(f"Product {code} is out of stock")
41 p.quantity -= 1
42 return p
43
44
45 class PaymentProcessor:
46 """Greedy change calculator using standard denominations."""
47
48 DENOMINATIONS = [100, 50, 25, 10, 5, 1]
49
50 def calculate_change(self, amount: int) -> list[int]:
51 coins = []
52 for denom in self.DENOMINATIONS:
53 while amount >= denom:
54 coins.append(denom)
55 amount -= denom
56 return coins
57
58
59 class State(ABC):
60 """State interface. Each state handles all possible user actions."""
61
62 @abstractmethod
63 def insert_money(self, machine: "VendingMachine", amount: int) -> None: ...
64
65 @abstractmethod
66 def select_product(self, machine: "VendingMachine", code: str) -> None: ...
67
68 @abstractmethod
69 def dispense(self, machine: "VendingMachine") -> None: ...
70
71 @abstractmethod
72 def cancel(self, machine: "VendingMachine") -> None: ...
73
74
75 class IdleState(State):
76 def insert_money(self, machine: "VendingMachine", amount: int) -> None:
77 machine.current_balance += amount
78 print(f" Accepted {amount} cents. Balance: {machine.current_balance}")
79 machine.set_state(HasMoneyState())
80
81 def select_product(self, machine: "VendingMachine", code: str) -> None:
82 print(" Insert money first.")
83
84 def dispense(self, machine: "VendingMachine") -> None:
85 print(" Insert money and select a product first.")
86
87 def cancel(self, machine: "VendingMachine") -> None:
88 print(" Nothing to cancel.")
89
90
91 class HasMoneyState(State):
92 def insert_money(self, machine: "VendingMachine", amount: int) -> None:
93 machine.current_balance += amount
94 print(f" Added {amount} cents. Balance: {machine.current_balance}")
95
96 def select_product(self, machine: "VendingMachine", code: str) -> None:
97 if not machine.inventory.is_available(code):
98 print(f" Product {code} is out of stock. Pick another or cancel.")
99 return
100
101 price = machine.inventory.get_price(code)
102 if machine.current_balance < price:
103 print(f" Not enough money. Need {price}, have {machine.current_balance}.")
104 return
105
106 machine.selected_product = code
107 print(f" Selected: {machine.inventory.get_product_name(code)} (price: {price})")
108 machine.set_state(DispensingState())
109 machine.dispense()
110
111 def dispense(self, machine: "VendingMachine") -> None:
112 print(" Select a product first.")
113
114 def cancel(self, machine: "VendingMachine") -> None:
115 change = machine.current_balance
116 machine.current_balance = 0
117 machine.set_state(IdleState())
118 print(f" Cancelled. Returning {change} cents.")
119
120
121 class DispensingState(State):
122 def insert_money(self, machine: "VendingMachine", amount: int) -> None:
123 print(" Please wait, dispensing in progress.")
124
125 def select_product(self, machine: "VendingMachine", code: str) -> None:
126 print(" Please wait, dispensing in progress.")
127
128 def dispense(self, machine: "VendingMachine") -> None:
129 code = machine.selected_product
130 price = machine.inventory.get_price(code)
131 product = machine.inventory.dispense(code)
132 machine.current_balance -= price
133 print(f" Dispensed: {product.name}")
134
135 if machine.current_balance > 0:
136 coins = machine.payment.calculate_change(machine.current_balance)
137 print(f" Change returned: {coins} (total: {machine.current_balance} cents)")
138 machine.current_balance = 0
139
140 machine.selected_product = None
141 machine.set_state(IdleState())
142
143 def cancel(self, machine: "VendingMachine") -> None:
144 print(" Cannot cancel during dispensing.")
145
146
147 class VendingMachine:
148 """Context object. Delegates all actions to the current state."""
149
150 def __init__(self):
151 self.inventory = Inventory()
152 self.payment = PaymentProcessor()
153 self.current_balance = 0
154 self.selected_product: str | None = None
155 self._state: State = IdleState()
156
157 def set_state(self, state: State) -> None:
158 self._state = state
159
160 def insert_money(self, amount: int) -> None:
161 self._state.insert_money(self, amount)
162
163 def select_product(self, code: str) -> None:
164 self._state.select_product(self, code)
165
166 def dispense(self) -> None:
167 self._state.dispense(self)
168
169 def cancel(self) -> None:
170 self._state.cancel(self)
171
172
173 if __name__ == "__main__":
174 vm = VendingMachine()
175
176 # Stock the machine
177 vm.inventory.add_product(Product("A1", "Cola", 150, 3))
178 vm.inventory.add_product(Product("A2", "Chips", 100, 2))
179 vm.inventory.add_product(Product("B1", "Water", 75, 5))
180
181 # Normal purchase with change
182 print("--- Purchase Cola (150 cents) with 200 ---")
183 vm.insert_money(100)
184 vm.insert_money(100)
185 vm.select_product("A1")
186
187 # Exact change purchase
188 print("\n--- Purchase Water (75 cents) ---")
189 vm.insert_money(75)
190 vm.select_product("B1")
191
192 # Insufficient funds
193 print("\n--- Insufficient funds ---")
194 vm.insert_money(50)
195 vm.select_product("A1")
196
197 # Cancel and get refund
198 print("\n--- Cancel ---")
199 vm.cancel()
200
201 # Out of stock scenario
202 print("\n--- Out of stock ---")
203 vm.inventory.add_product(Product("C1", "Candy", 50, 0))
204 vm.insert_money(100)
205 vm.select_product("C1")
206 vm.cancel()
207
208 print("\nAll scenarios completed.")Common Mistakes
- ✗Putting all state logic in one giant switch statement. Adding a new state means touching every case block.
- ✗Forgetting to return change when the user cancels. Money in the machine must always be accounted for.
- ✗Not checking stock before accepting money. The user inserts coins for an out-of-stock item and gets frustrated.
- ✗Allowing negative inventory. Every dispense must check stock first, then decrement.
Key Points
- ✓State pattern eliminates nested conditionals. Each state class handles only the actions valid for that state.
- ✓Transitions are explicit. IdleState moves to HasMoneyState on coin insert. No ambiguity.
- ✓Greedy algorithm for coin change works because standard denominations are canonical (each coin divides the next).
- ✓Inventory is separate from the state machine. Stock management and state transitions are independent concerns.