Low-Level Design · lesson 2 of 18
Beginner
4Intermediate
10- 5 Design a Parking Lot: Allocating and Freeing Spots
- 6 Design an Elevator System: Scheduling Requests
- 7 Design a Library Management System: Borrowing and Returning Books
- 8 Design an Expense-Splitting App: Balances and Minimal Settlements
- 9 Design a URL Shortener: Sequential Codes vs Real Hash Collisions
- 10 Wrapping Objects: Adapter, Facade, and Proxy in Python
- 11 The Composite Pattern in Python: Sizing a File Tree Without isinstance
- 12 The State Pattern in Python: Refactoring a Vending Machine Into State Classes
- 13 Undo and Redo in Python: the Memento and Command Patterns Compared
- 14 Chain of Responsibility in Python: Expense Approvals Without an if/elif Ladder
Advanced
4- 15 Concurrency in LLD: A Real Race Condition in the Parking Lot, Then the Fix
- 16 The Observer Pattern in Practice: A Stock Ticker Notifying Multiple Displays
- 17 Design Patterns That Are Just Language Features: Strategy, Command, Iterator
- 18 Abstract Factory and Prototype in Python: Matching Families and Cloning Objects
Also part of Low-Level Design
12- + SOLID Principles in Java from Java
- + Composition vs Inheritance in Java from Java
- + Design Patterns in Java from Java
- + Python Design Patterns from Python
- + Design Patterns in TypeScript from TypeScript
- + JavaScript Design Patterns from JavaScript
- + Design Patterns in C# from C#
- + Design Patterns in C++ from C++
- + Design Patterns in Go from Go
- + Design Patterns in Rust from Rust
- + Abstract Classes in Java from Java
- + Sealed Classes and Interfaces in Java from Java
Design a Vending Machine as a State Machine
Model IDLE, HAS_MONEY, DISPENSING, and SOLD_OUT as explicit states instead of scattered booleans, and watch invalid transitions get rejected by name.
By SkillByExample · Published
A vending machine only has a handful of states — but the wrong dispensing sequence in the wrong state is exactly how a machine gives away a snack for free or dispenses nothing.
Modeling the states explicitly
class VendingMachine:
def __init__(self):
self.state = "IDLE"
self._stock = 1
def insert_coin(self):
if self.state == "IDLE":
self.state = "HAS_MONEY"
print("Coin accepted. State -> HAS_MONEY")
else:
print(f"Cannot insert coin in state {self.state}")
def select_item(self):
if self.state != "HAS_MONEY":
print(f"Cannot select item in state {self.state}: insert coin first")
return
if self._stock <= 0:
self.state = "SOLD_OUT"
print("Out of stock. State -> SOLD_OUT")
return
self.state = "DISPENSING"
print("Item selected. State -> DISPENSING")
def dispense(self):
if self.state != "DISPENSING":
print(f"Cannot dispense in state {self.state}")
return
self._stock -= 1
self.state = "IDLE"
print(f"Dispensed. Stock left: {self._stock}. State -> IDLE")
m = VendingMachine()
m.select_item()
m.insert_coin()
m.select_item()
m.dispense()
m.insert_coin()
m.select_item()
$ python vending_fsm.py
Cannot select item in state IDLE: insert coin first
Coin accepted. State -> HAS_MONEY
Item selected. State -> DISPENSING
Dispensed. Stock left: 0. State -> IDLE
Coin accepted. State -> HAS_MONEY
Out of stock. State -> SOLD_OUT
The first select_item() call is rejected outright — the machine is still IDLE, so there’s
no money to spend yet. Notice the machine also catches its own last unit selling out and
moves itself to SOLD_OUT, rather than letting a caller dispense() on nothing.
Every method checks “am I in the right state?”
That’s the whole pattern: each method’s first line asks whether the current state permits the
requested action, and refuses — loudly, by printing what’s wrong — if it doesn’t. There’s no
scenario where dispense() can run without having gone through select_item() first, because
dispense() itself checks self.state != "DISPENSING" before doing anything.
Practice
1. Add a cancel method that refunds and returns to IDLE, but only from HAS_MONEY.
class VendingMachine:
def __init__(self):
self.state = "IDLE"
self._stock = 1
def insert_coin(self):
if self.state == "IDLE":
self.state = "HAS_MONEY"
print("Coin accepted. State -> HAS_MONEY")
else:
print(f"Cannot insert coin in state {self.state}")
def select_item(self):
if self.state != "HAS_MONEY":
print(f"Cannot select item in state {self.state}: insert coin first")
return
self.state = "DISPENSING"
print("Item selected. State -> DISPENSING")
def cancel(self):
if self.state != "HAS_MONEY":
print(f"Cannot cancel in state {self.state}")
return
self.state = "IDLE"
print("Refunded. State -> IDLE")
m = VendingMachine()
m.insert_coin()
m.cancel()
m.select_item()
$ python cancel.py
Coin accepted. State -> HAS_MONEY
Refunded. State -> IDLE
Cannot select item in state IDLE: insert coin first
cancel() follows the exact same rule as every other method: check the current state first.
After refunding, the machine is back to IDLE, so select_item() correctly refuses again.
2. Replace the if/elif chain with a single transition table.
class VendingMachine:
VALID_TRANSITIONS = {
"IDLE": {"insert_coin": "HAS_MONEY"},
"HAS_MONEY": {"select_item": "DISPENSING"},
"DISPENSING": {"dispense": "IDLE"},
}
def __init__(self):
self.state = "IDLE"
def transition(self, event):
allowed = self.VALID_TRANSITIONS.get(self.state, {})
if event not in allowed:
print(f"Invalid event '{event}' in state {self.state}")
return
self.state = allowed[event]
print(f"{event} -> {self.state}")
m = VendingMachine()
m.transition("select_item")
m.transition("insert_coin")
m.transition("insert_coin")
m.transition("select_item")
$ python transition_table.py
Invalid event 'select_item' in state IDLE
insert_coin -> HAS_MONEY
Invalid event 'insert_coin' in state HAS_MONEY
select_item -> DISPENSING
One dictionary now encodes every legal move. Adding a fifth state later means adding one
entry to VALID_TRANSITIONS, not editing five separate methods.
3. Track total revenue across multiple purchases, requiring exact payment.
class VendingMachine:
PRICE = 15
def __init__(self):
self.state = "IDLE"
self._stock = 2
self.revenue = 0
def insert_coin(self, amount):
if self.state == "IDLE":
self.state = "HAS_MONEY"
self._balance = amount
print(f"Coin accepted ({amount}). State -> HAS_MONEY")
else:
print(f"Cannot insert coin in state {self.state}")
def select_item(self):
if self.state != "HAS_MONEY":
print(f"Cannot select item in state {self.state}: insert coin first")
return
if self._balance < self.PRICE:
print(f"Cannot select item: need {self.PRICE}, have {self._balance}")
return
self.state = "DISPENSING"
print("Item selected. State -> DISPENSING")
def dispense(self):
if self.state != "DISPENSING":
print(f"Cannot dispense in state {self.state}")
return
self._stock -= 1
self.revenue += self.PRICE
self.state = "IDLE"
print(f"Dispensed. Revenue so far: {self.revenue}. State -> IDLE")
m = VendingMachine()
m.insert_coin(15)
m.select_item()
m.dispense()
m.insert_coin(15)
m.select_item()
m.dispense()
print(f"Total revenue: {m.revenue}")
$ python revenue.py
Coin accepted (15). State -> HAS_MONEY
Item selected. State -> DISPENSING
Dispensed. Revenue so far: 15. State -> IDLE
Coin accepted (15). State -> HAS_MONEY
Item selected. State -> DISPENSING
Dispensed. Revenue so far: 30. State -> IDLE
Total revenue: 30
revenue only ever increases inside dispense(), the one place a sale is actually confirmed
— so it can’t drift out of sync with how many items were really handed out.
Next: Tic-Tac-Toe — a board, two players, and win detection that has to check every row, column, and diagonal.