Skip to content
LLD Learn/Case Studies: Physical Systems
Browsing as a guest. Sign in to save your progress and earn XP as you complete chapters.

Design a Vending Machine

8 min read

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

The machine delegates every action to its current state
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

IdleState, HasMoneyState, and DispensingState
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.

ScaleDojo Logo
Initializing ScaleDojo