Low-Level Design · lesson 1 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
Why Class Design Matters: A Naive Vending Machine vs a Designed One
A vending machine that lets stock go negative, then one that refuses cleanly — the smallest demonstration of why encapsulation is LLD's first move.
By SkillByExample · Published
A vending machine is the simplest possible LLD problem — one class, one method, one bug.
The naive version
class NaiveVendingMachine:
def __init__(self):
self.stock = {"soda": 2, "chips": 1}
def dispense(self, item):
self.stock[item] -= 1
print(f"Dispensed {item}. Remaining: {self.stock[item]}")
machine = NaiveVendingMachine()
machine.dispense("chips")
machine.dispense("chips")
machine.dispense("chips")
$ python naive_vending.py
Dispensed chips. Remaining: 0
Dispensed chips. Remaining: -1
Dispensed chips. Remaining: -2
There’s only one bag of chips, but the machine “dispenses” three. self.stock is a plain
public dict — nothing stops dispense from decrementing it below zero, and nothing stops
some other part of the program from writing machine.stock["chips"] = 999 directly either.
The designed version
class VendingMachine:
def __init__(self):
self._stock = {"soda": 2, "chips": 1}
def dispense(self, item):
if self._stock.get(item, 0) <= 0:
print(f"Cannot dispense {item}: out of stock")
return
self._stock[item] -= 1
print(f"Dispensed {item}. Remaining: {self._stock[item]}")
machine = VendingMachine()
machine.dispense("chips")
machine.dispense("chips")
machine.dispense("chips")
$ python vending.py
Dispensed chips. Remaining: 0
Cannot dispense chips: out of stock
Cannot dispense chips: out of stock
Same three calls, same starting stock — but now the machine refuses instead of lying. The
fix isn’t the if check alone; it’s that _stock is only ever touched through dispense,
so the check can’t be bypassed by a caller who forgets to add it themselves. This is the
whole idea of encapsulation: an object protects its own invariants instead of trusting every
caller to protect them for it.
Practice
1. Add a refill method that adds stock back.
class VendingMachine:
def __init__(self):
self._stock = {"soda": 2, "chips": 1}
def dispense(self, item):
if self._stock.get(item, 0) <= 0:
print(f"Cannot dispense {item}: out of stock")
return
self._stock[item] -= 1
print(f"Dispensed {item}. Remaining: {self._stock[item]}")
def refill(self, item, quantity):
self._stock[item] = self._stock.get(item, 0) + quantity
print(f"Refilled {item}. New stock: {self._stock[item]}")
machine = VendingMachine()
machine.dispense("chips")
machine.dispense("chips")
machine.refill("chips", 3)
machine.dispense("chips")
$ python refill.py
Dispensed chips. Remaining: 0
Cannot dispense chips: out of stock
Refilled chips. New stock: 3
Dispensed chips. Remaining: 2
refill is the only other place _stock is written, so the invariant (never negative) only
has two gatekeepers to check instead of every line in the program.
2. Require payment before dispensing, refusing if the balance is too low.
class VendingMachine:
PRICES = {"soda": 15, "chips": 10}
def __init__(self):
self._stock = {"soda": 2, "chips": 1}
self._balance = 0
def insert_coin(self, amount):
self._balance += amount
print(f"Balance: {self._balance}")
def dispense(self, item):
price = self.PRICES[item]
if self._stock.get(item, 0) <= 0:
print(f"Cannot dispense {item}: out of stock")
return
if self._balance < price:
print(f"Cannot dispense {item}: need {price}, have {self._balance}")
return
self._stock[item] -= 1
self._balance -= price
print(f"Dispensed {item}. Change: {self._balance}")
machine = VendingMachine()
machine.insert_coin(10)
machine.dispense("soda")
machine.insert_coin(10)
machine.dispense("soda")
$ python priced_vending.py
Balance: 10
Cannot dispense soda: need 15, have 10
Balance: 20
Dispensed soda. Change: 5
Two invariants now, both enforced inside dispense: never dispense out-of-stock, and never
dispense unpaid. Neither check can be skipped by a caller.
3. Replace the printed error with a custom OutOfStockError exception.
class OutOfStockError(Exception):
pass
class VendingMachine:
def __init__(self):
self._stock = {"soda": 2, "chips": 1}
def dispense(self, item):
if self._stock.get(item, 0) <= 0:
raise OutOfStockError(f"{item} is out of stock")
self._stock[item] -= 1
print(f"Dispensed {item}. Remaining: {self._stock[item]}")
machine = VendingMachine()
machine.dispense("chips")
try:
machine.dispense("chips")
except OutOfStockError as e:
print(f"Error: {e}")
$ python exception_vending.py
Dispensed chips. Remaining: 0
Error: chips is out of stock
Raising forces every caller to handle the failure explicitly — an uncaught OutOfStockError
crashes loudly, where the earlier print-and-return version could be silently ignored by a
caller who never checks what dispense printed.
Next: a state machine that tracks money and dispensing as explicit states, not just a stock count.