Design a Vending Machine
You'll learn to
- -Model the machine as a State-pattern state machine (idle, has-money, dispensing, out-of-stock)
- -Handle inventory, change-making, and payment failure as first-class states rather than special-case conditionals
A vending machine is the cleanest possible showcase for the State pattern covered earlier in this course - its entire behavior is genuinely, obviously state-dependent (what happens when you press "select" is completely different depending on whether money has been inserted), which makes it a natural fit rather than a forced one.
Step 1: Requirements and the State Set
Assume: the machine holds an inventory of items with prices, accepts coins one at a time, lets the user select an item once enough money is inserted, dispenses the item and any change, and can be out of stock for a specific item or fully sold out. That maps to four core states: Idle, HasMoney, Dispensing, and OutOfStock (or SoldOut).
Step 2: The State Interface and Context
class VendingMachineState(ABC):
@abstractmethod
def insert_coin(self, machine: "VendingMachine", amount: float) -> None: ...
@abstractmethod
def select_item(self, machine: "VendingMachine", item_code: str) -> None: ...
@abstractmethod
def dispense(self, machine: "VendingMachine") -> None: ...
class VendingMachine:
def __init__(self, inventory: dict[str, "Item"]):
self.inventory = inventory
self.balance = 0.0
self.selected_item: "Item | None" = None
self.state: VendingMachineState = IdleState()
def insert_coin(self, amount: float) -> None:
self.state.insert_coin(self, amount)
def select_item(self, item_code: str) -> None:
self.state.select_item(self, item_code)
def dispense(self) -> None:
self.state.dispense(self)Step 3: Each State Owns Its Own Valid Transitions
class IdleState(VendingMachineState):
def insert_coin(self, machine, amount):
machine.balance += amount
machine.state = HasMoneyState()
def select_item(self, machine, item_code):
print("Insert coins first")
def dispense(self, machine):
print("Nothing to dispense")
class HasMoneyState(VendingMachineState):
def insert_coin(self, machine, amount):
machine.balance += amount # allow adding more coins
def select_item(self, machine, item_code):
item = machine.inventory.get(item_code)
if not item or item.quantity == 0:
print("Item unavailable")
return
if machine.balance < item.price:
print("Insufficient funds")
return
machine.selected_item = item
machine.state = DispensingState()
class DispensingState(VendingMachineState):
def insert_coin(self, machine, amount):
print("Please wait, dispensing in progress")
def select_item(self, machine, item_code):
print("Already dispensing")
def dispense(self, machine):
item = machine.selected_item
item.quantity -= 1
change = machine.balance - item.price
print(f"Dispensing {item.name}, change: {change}")
machine.balance = 0.0
machine.selected_item = None
machine.state = OutOfStockState() if item.quantity == 0 else IdleState()Why This Design Absorbs Follow-Ups Cleanly
Notice that DispensingState transitions directly to OutOfStockState() when the last unit of an item sells out - inventory-driven state transitions fall out naturally from the pattern rather than needing a separate "check inventory everywhere" concern bolted on. If the interviewer asks "what if a user cancels and wants a refund mid-transaction," the answer is a new method on the interface (cancel()) implemented per-state: HasMoneyState.cancel() refunds and returns to Idle, while IdleState.cancel() and DispensingState.cancel() can each define their own (likely no-op or rejected) behavior - no existing transition logic needs to change.
Compare this design explicitly against the "if state == X" conditional version from the State pattern chapter when you present it - naming that comparison out loud is exactly the kind of principle-grounded reasoning interviewers are listening for.
Interview Signal is part of Pro
See a real weak answer next to a real strong one for this exact topic.
Quiz is part of Pro
Test what you just read with a short quiz, and bank the XP.