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