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