Low-Level Design: Vending Machine (OOP Interview)

Low-Level Design: Vending Machine (OOP Interview)

The vending machine is a classic state machine design question. It tests whether you can model discrete states and transitions cleanly, apply the State pattern, and handle edge cases like insufficient funds and empty slots. Here is the complete implementation.

State Machine Overview


IDLE ──── insert_coin() ──→ HAS_MONEY
  ↑                             │
  │                        select_item()
  │                             │
  │                     ┌───────┴───────┐
  │                     ↓               ↓
  │              DISPENSING        OUT_OF_STOCK
  │                     │               │
  └─── dispense() ──────┘    return_coin()
                                         │
                                        IDLE

State Pattern Implementation

from abc import ABC, abstractmethod
from enum import Enum
from dataclasses import dataclass
from typing import Optional

class VendingMachineState(ABC):
    """Each state handles its allowed actions; raises error for invalid transitions."""

    @abstractmethod
    def insert_coin(self, machine: 'VendingMachine', amount: float) -> None:
        pass

    @abstractmethod
    def select_item(self, machine: 'VendingMachine', item_code: str) -> None:
        pass

    @abstractmethod
    def dispense(self, machine: 'VendingMachine') -> Optional[str]:
        pass

    @abstractmethod
    def return_coin(self, machine: 'VendingMachine') -> float:
        pass

class InvalidActionError(Exception):
    pass

@dataclass
class Item:
    code:     str
    name:     str
    price:    float
    quantity: int

    def is_available(self) -> bool:
        return self.quantity > 0

Concrete States

class IdleState(VendingMachineState):
    def insert_coin(self, machine: 'VendingMachine', amount: float) -> None:
        if amount  None:
        machine.balance += amount
        print(f"Added $" + f"{amount:.2f}. Total balance: $" + f"{machine.balance:.2f}")

    def select_item(self, machine: 'VendingMachine', item_code: str) -> None:
        item = machine.inventory.get(item_code)
        if not item:
            raise ValueError(f"Unknown item code: {item_code}")
        if not item.is_available():
            print(f"{item.name} is out of stock.")
            machine.set_state(machine.out_of_stock_state)
            return
        if machine.balance  float:
        change = machine.balance
        machine.balance = 0
        machine.set_state(machine.idle_state)
        print(f"Returned $" + f"{change:.2f}")
        return change

class DispensingState(VendingMachineState):
    def insert_coin(self, machine, amount):
        raise InvalidActionError("Currently dispensing, please wait")

    def select_item(self, machine, item_code):
        raise InvalidActionError("Currently dispensing, please wait")

    def dispense(self, machine: 'VendingMachine') -> str:
        item = machine.selected_item
        change = machine.balance - item.price
        item.quantity -= 1
        machine.balance = 0
        machine.selected_item = None
        machine.set_state(machine.idle_state)

        print(f"Dispensing {item.name}!")
        if change > 0:
            print(f"Change returned: $" + f"{change:.2f}")
        return item.name

    def return_coin(self, machine):
        raise InvalidActionError("Cannot return coins while dispensing")

class OutOfStockState(VendingMachineState):
    def insert_coin(self, machine, amount):
        raise InvalidActionError("Machine is out of stock for selected item")

    def select_item(self, machine, item_code):
        raise InvalidActionError("Selected item is out of stock")

    def dispense(self, machine):
        raise InvalidActionError("Item is out of stock")

    def return_coin(self, machine: 'VendingMachine') -> float:
        change = machine.balance
        machine.balance = 0
        machine.set_state(machine.idle_state)
        print(f"Item out of stock. Returned $" + f"{change:.2f}")
        return change

VendingMachine (Context)

class VendingMachine:
    def __init__(self, inventory: dict[str, Item]):
        # States
        self.idle_state        = IdleState()
        self.has_money_state   = HasMoneyState()
        self.dispensing_state  = DispensingState()
        self.out_of_stock_state = OutOfStockState()

        self._state: VendingMachineState = self.idle_state
        self.inventory: dict[str, Item] = inventory
        self.balance:   float = 0.0
        self.selected_item: Optional[Item] = None

    def set_state(self, state: VendingMachineState) -> None:
        print(f"[State] {type(self._state).__name__} → {type(state).__name__}")
        self._state = state

    # Public interface — delegates to current state
    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) -> Optional[str]:
        return self._state.dispense(self)

    def return_coin(self) -> float:
        return self._state.return_coin(self)

    def restock(self, item_code: str, quantity: int) -> None:
        if item_code in self.inventory:
            self.inventory[item_code].quantity += quantity
            print(f"Restocked {item_code}: +{quantity} units")

    def status(self) -> dict:
        return {
            "state":   type(self._state).__name__,
            "balance": self.balance,
            "items":   {code: {"name": i.name, "qty": i.quantity, "price": i.price}
                        for code, i in self.inventory.items()}
        }

Usage Example

inventory = {
    "A1": Item("A1", "Cola",   1.50, quantity=5),
    "A2": Item("A2", "Water",  1.00, quantity=3),
    "B1": Item("B1", "Chips",  2.00, quantity=0),  # out of stock
}

vm = VendingMachine(inventory)

# Happy path
vm.insert_coin(1.00)     # Idle → HasMoney
vm.insert_coin(0.75)     # HasMoney (add more)
vm.select_item("A1")     # HasMoney → Dispensing → Idle
# → "Dispensing Cola! Change returned: $0.25"

# Insufficient funds
vm.insert_coin(0.50)
vm.select_item("A2")     # price is $1.00, balance is $0.50 → stays in HasMoney
vm.insert_coin(0.50)     # top up
vm.select_item("A2")     # now succeeds

# Out of stock
vm.insert_coin(2.00)
vm.select_item("B1")     # HasMoney → OutOfStock
# → "Chips is out of stock."
vm.return_coin()         # → "Returned $2.00"

# Invalid action
try:
    vm.select_item("A1")  # in Idle state — raises InvalidActionError
except InvalidActionError as e:
    print(e)

Why the State Pattern?

Without the State pattern, the VendingMachine would have a massive switch-case or chain of if/elif statements in each method, checking the current state. Adding a new state (e.g., MAINTENANCE_MODE) would require modifying every method. With the State pattern: add a new state class, implement the 4 methods, and transition to it where needed — existing states are unchanged (Open/Closed Principle).

Extensibility

  • Payment methods: Add PaymentStrategy (coin, card, contactless) injected into HasMoneyState
  • Admin interface: Add AdminState with restock() and price_change() methods, entered via a key switch
  • Logging/metrics: Wrap set_state() with observer notifications (transition events sent to analytics)
  • Concurrency: Add a threading.Lock in VendingMachine; acquire before any state-mutating operation

{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How does the State pattern apply to a vending machine design?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”A vending machine has discrete states (IDLE, HAS_MONEY, DISPENSING, OUT_OF_STOCK) and each state allows only certain actions. Without the State pattern, you’d have a large if/elif chain in every method checking the current state — unmaintainable as states grow. With State pattern: create a VendingMachineState abstract class with methods for each action. Each concrete state class implements the methods: valid transitions perform the action and switch states; invalid transitions raise InvalidActionError. Adding a new state (e.g., MAINTENANCE) only requires a new class — no modification to existing states (Open/Closed Principle). The VendingMachine context delegates all actions to the current state object.”}},{“@type”:”Question”,”name”:”What are the valid state transitions in a vending machine?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”IDLE → HAS_MONEY (insert_coin). HAS_MONEY → HAS_MONEY (insert_coin adds more). HAS_MONEY → DISPENSING (select_item with sufficient funds + item available). HAS_MONEY → OUT_OF_STOCK (select_item but item quantity is 0). HAS_MONEY → IDLE (return_coin). DISPENSING → IDLE (after dispense completes). OUT_OF_STOCK → IDLE (return_coin). Invalid transitions (raise errors): select_item in IDLE, insert_coin in DISPENSING, dispense in OUT_OF_STOCK. The machine should never reach an undefined state — every action either performs a valid transition or raises a clear error.”}},{“@type”:”Question”,”name”:”How do you handle change calculation in a vending machine design?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Track balance (total coins inserted) as a float on the VendingMachine context. On dispense: change = balance – item.price. Return change before resetting balance. For a physical machine with coin denominations, implement a coin change algorithm: greedily dispense largest denominations first (quarters, dimes, nickels, pennies) while change > 0. Edge case: if exact change cannot be made from available coins, either refuse the transaction (return all coins, stay in HAS_MONEY) or offer a credit for next purchase. In interview context, abstract this into a ChangeCalculator strategy injected into DispensingState — allows testing different change algorithms independently.”}}]}

🏢 Asked at: Atlassian Interview Guide

🏢 Asked at: Meta Interview Guide 2026: Facebook, Instagram, WhatsApp Engineering

🏢 Asked at: Apple Interview Guide 2026: iOS Systems, Hardware-Software Integration, and iCloud Architecture

🏢 Asked at: Uber Interview Guide 2026: Dispatch Systems, Geospatial Algorithms, and Marketplace Engineering

Asked at: Shopify Interview Guide

Asked at: LinkedIn Interview Guide

See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering

See also: Netflix Interview Guide 2026: Streaming Architecture, Recommendation Systems, and Engineering Excellence

Scroll to Top