Building a vending machine
Implement the State pattern in code, plus the greedy change-making algorithm and the exact-change and concurrency edge cases interviewers probe.
The State interface
Every state implements the same four actions; the machine delegates to its current state and never branches on flags:
class State: # interface
def insert_money(self, machine, amount): ...
def select(self, machine, code): ...
def dispense(self, machine): ...
def cancel(self, machine): ...
class VendingMachine:
def __init__(self, inventory):
self.inventory = inventory # code -> (product, price, qty)
self.balance = 0
self.selected = None
self.state = NoMoneyState()
# public API just forwards to the current state
def insert_money(self, amt): self.state.insert_money(self, amt)
def select(self, code): self.state.select(self, code)
def cancel(self): self.state.cancel(self)
The states
Each state handles actions appropriate to it and rejects the rest, transitioning the machine as needed:
class NoMoneyState(State):
def insert_money(self, m, amt):
m.balance += amt
m.state = HasMoneyState()
def select(self, m, code):
print("Insert money first") # rejected in this state
def cancel(self, m):
print("Nothing to cancel")
class HasMoneyState(State):
def insert_money(self, m, amt):
m.balance += amt # add more
def select(self, m, code):
product, price, qty = m.inventory[code]
if qty == 0:
m.state = SoldOutState(); print("Sold out")
elif m.balance < price:
print(f"Need {price - m.balance} more")
else:
m.selected = code
m.state = DispenseState()
m.state.dispense(m) # proceed immediately
def cancel(self, m):
refund, m.balance = m.balance, 0
m.state = NoMoneyState()
print("Refunded", refund)
class DispenseState(State):
def dispense(self, m):
product, price, qty = m.inventory[m.selected]
change = m.balance - price
if not m.can_make_change(change): # exact-change guard
m.cancel(); print("Can't make change"); return
m.inventory[m.selected] = (product, price, qty - 1)
m.give_change(change)
m.balance, m.selected = 0, None
m.state = SoldOutState() if qty - 1 == 0 else NoMoneyState()
print("Dispensed", product)
The shape to notice: no method anywhere branches on “do we have money?” — the state class itself encodes that, so each method is short and the illegal paths are handled locally.
Change-making (greedy, and its caveat)
Returning change is the coin change problem. With standard currency denominations, greedy (take the largest coin ≤ remaining, repeat) is optimal and simple:
def make_change(amount, coins_available): # coins_available: denom -> count
result = {}
for denom in sorted(coins_available, reverse=True):
take = min(amount // denom, coins_available[denom])
if take:
result[denom] = take
amount -= take * denom
return result if amount == 0 else None # None → can't make exact change
The caveat worth stating: greedy is only optimal for canonical coin systems
(like USD/EUR). For arbitrary denominations it can fail, and you’d fall back to the
DP coin-change algorithm. Returning None drives the exact-change-only state.
Edge cases interviewers probe
- Insufficient funds — stay in HasMoney, ask for more (handled above).
- Exact change unavailable — refuse and refund (the
can_make_changeguard); show an “exact change only” indicator when the coin reserve is low. - Sold out — transition to SoldOutState; refund any inserted money.
- Cancel mid-transaction — always refund the full balance.
- Concurrency — a single physical machine is effectively single-threaded for vends, but if modeling a networked machine, guard the decrement-inventory-and-dispense as an atomic critical section so two requests can’t dispense the last item.
The takeaway
The graded signal: a clean State pattern (behavior per state, no flag soup), a correct greedy change with the canonical-currency caveat, and explicit handling of exact-change and sold-out. With both warm-ups done, you’ve shown the OOD fundamentals — the rest of the chapter moves to distributed scale, where these modeling instincts still anchor every data model and component.