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

🏢 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