What Is Low-Level Design? The LLD Interview Explained
8 min readBytePatterns
What low-level design is: classes, responsibilities and the calls between them, how it differs from system design, and a step-by-step LLD interview method.
System design interviews ask which services exist and how data flows between them. Low-level design (LLD) asks the next question down: inside one of those services, which classes exist, what each one is responsible for, and how they call each other. It is where "design a parking lot" and "design an elevator" live, and it is graded less on the classes you name than on where you draw the lines between them.
The problem it solves
Code that works today still has to survive next month's requirement. Without deliberate structure, every class knows about every other class, and a pricing change edits the booking code, the seat code and the API handler. LLD is the discipline of putting each decision in exactly one place, so a change stays local.
The contrast with high-level design (HLD) is scale and vocabulary:
- HLD: services, databases, caches, queues, traffic numbers. Boxes are processes; arrows are network calls. See system design interview questions.
- LLD: classes, interfaces, methods, state and invariants inside one process. Boxes are objects; arrows are method calls.
The intuition
A reliable order of work, which is also the lesson's exercise:
- Clarify the requirements. List the actions the system must support, in plain sentences: search trips, hold a seat, quote a price, pay. Ask what is out of scope.
- Pull out the nouns. The nouns in those sentences are candidate classes: Trip, Seat map, Price. Some will merge, some will vanish.
- Fix responsibilities. For each class, say what it owns (data and the rules about it) and what it refuses to do. This is the hard part, and the part interviewers probe.
- Define the methods. Only now write the calls objects make on each other. Prefer "tell, don't ask": Trip tells the seat map to reserve, rather than reading its free-seat set and editing it.
- Test with a change. Propose a new requirement and count the classes it touches. One or two means the seams are in the right place.
Design patterns come last, if at all. A strategy or a factory is a name for a seam you already found, not a goal. The principles underneath, single responsibility, composition over inheritance and programming to interfaces, are summarised in SOLID explained.
Watch it run
The animation zooms from one level to the next. High-level design stops at the top row: which services exist, api, booking and payments, and who calls whom. Low-level design opens one of the boxes and asks what lives inside booking. Not classes yet: first the requirements, the actions this service has to support, search, hold a seat, quote and pay. The nouns in those sentences are the candidate classes, Trip, SeatMap and Price. Then the hard part: what each one owns, and what it refuses to do. SeatMap owns which seats are free, and Trip is not allowed to reach in and edit them. Then the methods, the calls the objects make on one another: Trip asks SeatMap to reserve 12A. Trip asks, SeatMap answers, and neither reads the other's fields. Pricing changes weekly, so it sits behind its own seam rather than inside Trip, quoting £240. The test of the whole design is a new rule, group discounts, which touches one class, one file. The last frame is the counterweight: design all of it up front and you freeze decisions you do not understand yet, cost before evidence.
What Is Low-Level Design
Step 1 of 11
High-level design stops here: which services exist, and who calls whom.
The same interactive animation as the lesson — step through it with the controls.
The code
A toy model of the animation's booking service. Each class owns one thing; Trip only calls methods:
class SeatMap:
"""Owns which seats are free. Nobody else edits that set."""
def __init__(self, seats):
self._free = set(seats)
def free(self):
return sorted(self._free)
def reserve_all(self, seats): # all or nothing: SeatMap's rule, not Trip's
if len(set(seats)) != len(seats) or not set(seats) <= self._free:
return False
self._free -= set(seats)
return True
class Price:
"""Owns how a quote is computed: the rule most likely to change."""
def __init__(self, per_seat):
self.per_seat = per_seat
def quote(self, party):
return self.per_seat * party
class Trip:
"""Owns one booking. Asks SeatMap and Price; never reads their fields."""
def __init__(self, party, seats, price):
self.party, self.held = party, []
self._seats, self._price = seats, price
def hold(self, wanted):
if len(wanted) != self.party or not self._seats.reserve_all(wanted):
return None
self.held = list(wanted)
return self._price.quote(self.party)
seats = SeatMap(["11A", "11B", "12A", "12B"])
trip = Trip(2, seats, Price(per_seat=120))
print(trip.hold(["12A", "12B"]), seats.free()) # 240 ['11A', '11B']
print(Trip(2, seats, Price(120)).hold(["11A", "12A"]), seats.free()) # None ['11A', '11B']
The second hold fails as a whole, and 11A stays free: all-or-nothing lives in SeatMap, so no caller can get it half right. Now the change test. Group discounts, 10% off for parties of six or more, arrive as one new class; Trip and SeatMap are not edited:
class GroupPrice(Price): # the new rule: one new class, nothing else edited
def quote(self, party):
full = super().quote(party)
return full * 9 // 10 if party >= 6 else full
big = Trip(6, SeatMap([f"{r}{c}" for r in (1, 2) for c in "ABC"]), GroupPrice(120))
print(big.hold(["1A", "1B", "1C", "2A", "2B", "2C"])) # 648
Finally, a refactor-equivalence check. The "before" is one procedural function that knows everything. On 2,000 seeded scenarios, with random seat maps, party sizes, duplicate and wrong-length requests and both pricing rules, the object design must return the same quotes and leave the same seats free after every request:
import random
def book_procedural(free, party, wanted, per_seat, group):
"""The 'before': one function that knows everything."""
if len(wanted) != party or len(set(wanted)) != party or not set(wanted) <= free:
return None, free
total = per_seat * party
if group and party >= 6:
total = total * 9 // 10
return total, free - set(wanted)
rng = random.Random(35)
ok = True
for _ in range(2000):
all_seats = [f"{r}{c}" for r in range(1, rng.randint(2, 6)) for c in "ABCD"]
group, per_seat = rng.random() < 0.5, rng.randint(10, 300)
seat_map, free = SeatMap(all_seats), set(all_seats)
for _ in range(rng.randint(1, 6)):
party = rng.randint(1, 8)
wanted = [rng.choice(all_seats) for _ in range(rng.choice([party, party, party + 1]))]
got = Trip(party, seat_map, (GroupPrice if group else Price)(per_seat)).hold(wanted)
want, free = book_procedural(free, party, wanted, per_seat, group)
ok &= got == want and seat_map.free() == sorted(free)
print(ok) # True
Same behaviour, different shape. The procedural version is shorter today; the object version is the one where the next pricing rule, a seat-class surcharge or a loyalty discount, touches a single class.
The complexity
LLD is judged on change cost rather than running time:
- Classes touched per new requirement: the main metric. One or two is healthy.
- Dependencies per class: fewer collaborators means fewer reasons to change.
- Runtime: still matters for the hot method, such as an
O(1)lookup in an LRU cache.
Where it goes wrong
- Starting with classes. Requirements decide which objects exist; skipping them produces classes nobody needs.
- God classes. A
BookingManagerthat owns seats, prices and payments is the procedural function with a class keyword. - Getters everywhere. If Trip reads SeatMap's set and edits it, the seam is fake.
- Patterns for their own sake. Naming a pattern before finding a seam adds indirection without benefit.
- Over-design. Interfaces for things that will never vary cost time now and readability forever.
When it shows up in interviews
As object-oriented design or "machine coding" rounds: a parking lot, an elevator, a vending machine, a chess game or a file system. As of October 2026, round names and formats vary between companies, from whiteboard class diagrams to an hour of runnable code (from memory). The interviewer will add a requirement halfway through; that is the change test, live.
How to say it in an interview
"Low-level design is the inside of one service: classes, their responsibilities, and the calls between them. I start from requirements, list the actions, take the nouns as candidate classes, then decide what each class owns and what it refuses to do. Only then do I define methods, preferring to tell an object what to do rather than reach into its state. I check the design by adding a new requirement: if it touches one or two classes, the seams are right. Patterns come in only where they name a seam I already need."