The Object-Oriented / Low-Level Design Round
A different interview from the distributed-systems one: no servers, no scale — just classes,
interfaces, and design patterns. Very common in India and Pakistan, at Amazon, and at product
companies, and it has its own framework.
Prerequisites: Layered & Hexagonal Architecture, Interview Formats
Time to read: ~18 minutes
What LLD is (and isn’t)
🚨 Low-Level Design (LLD) is the object-oriented design round: “Design the classes for a parking lot
/ elevator / vending machine / chess game.” No load balancers, no databases, no scale — it’s about
classes, their responsibilities, relationships, and the design patterns that make the code extensible.
🚨 It’s a distinct interview, common in specific places (interview formats):
very common in India and Pakistan (Tkxel, Systems Ltd, Arbisoft, most product companies), at Amazon (as
part of the loop), and at Uber/Flipkart-style companies. If you’re targeting those, you must prepare
for it separately — the distributed-systems framework doesn’t apply.
What it tests: clean OOP, SOLID principles, appropriate design patterns, and — the real test —
whether your design extends gracefully when the interviewer adds a requirement at minute 35.
The LLD framework
🚨 A structure parallel to the distributed-systems framework, for LLD:
1. Requirements & clarify (5 min) — features, actors, use cases
2. Identify entities/classes (5 min) — the nouns; core objects
3. Define relationships (5 min) — has-a, is-a, uses; associations
4. Class diagram / interfaces (10 min) — attributes, methods, interfaces
5. Apply design patterns (10 min) — where patterns fit
6. Handle extensions/edge cases (5 min) — the "now add X" test
Like the other framework, the value is in having a structure so you don’t freeze.
Step-by-step: designing a parking lot
1. Clarify. Types of vehicles (car, bike, truck)? Types of spots (compact, large, handicapped)?
Multiple floors? Payment? Ticketing? → Scope it.
2. Identify entities (the nouns):
ParkingLot, ParkingFloor, ParkingSpot, Vehicle, Ticket, Payment, EntryGate, ExitGate
🚨 Nouns become classes — a useful heuristic for finding entities.
3. Relationships:
ParkingLot has-many ParkingFloor
ParkingFloor has-many ParkingSpot
Vehicle is-a: Car, Bike, Truck (inheritance)
ParkingSpot is-a: CompactSpot, LargeSpot (inheritance)
Ticket associates a Vehicle with a ParkingSpot
4. Class design (attributes + methods + interfaces):
abstract Vehicle { licensePlate; VehicleType type; }
class Car extends Vehicle { ... }
abstract ParkingSpot { id; boolean isAvailable; canFit(Vehicle); }
class CompactSpot extends ParkingSpot { canFit(v) → v.type == BIKE || CAR }
class ParkingLot {
parkVehicle(Vehicle) → Ticket // finds a spot, issues a ticket
unpark(Ticket) → Payment
}
5. Apply patterns (where they genuinely fit):
- Strategy for pricing (hourly, daily, flat) — swap pricing algorithms.
- Factory for creating vehicles/spots.
- Singleton for the ParkingLot instance (used judiciously).
- Observer for notifying when a spot frees up.
6. Extensions (the real test): “Now support electric-vehicle charging spots.” → Does your design
handle it cleanly (add a subclass), or does it require rewriting? 🚨 This is what’s actually being
graded.
SOLID — the principles they’re looking for
🚨 LLD interviews specifically probe SOLID. Know each and apply it:
- S — Single Responsibility. Each class does one thing. (A
Ticket doesn’t also process payments.)
- O — Open/Closed. Open for extension, closed for modification. 🚨 The big one — adding a new
vehicle type should mean adding a class, not editing existing ones. This is what the “extension” test
checks.
- L — Liskov Substitution. Subclasses must be substitutable for their base. (A
Bike used anywhere a
Vehicle is expected must work.)
- I — Interface Segregation. Many specific interfaces over one fat one. (Don’t force a class to
implement methods it doesn’t need.)
- D — Dependency Inversion. Depend on abstractions, not concretions. → Hexagonal.
(
ParkingLot depends on a PricingStrategy interface, not a concrete HourlyPricing.)
🎙️ “I’ll make the pricing a Strategy so we satisfy open/closed — adding a new pricing model is a new
class implementing the interface, not a change to existing code.”
The design patterns worth knowing
🚨 You don’t need all 23 GoF patterns, but know these common ones and where they apply:
| Pattern |
Use for |
Example |
| Strategy |
Swappable algorithms |
Pricing, sorting, routing |
| Factory |
Object creation |
Creating vehicles/spots by type |
| Singleton |
One instance |
A config, a connection pool (use judiciously) |
| Observer |
Notify on change |
Spot freed → notify waiting cars |
| Decorator |
Add behaviour dynamically |
Add features to a base object |
| State |
Behaviour depends on state |
A vending machine, an order lifecycle |
| Command |
Encapsulate a request |
Undo/redo, queuing operations |
| Builder |
Complex object construction |
Building a configured object step by step |
🚨 Apply patterns because they fit, not to show off. Forcing a pattern where a simple method would do
is a negative signal (over-engineering, LLD version). The interviewer wants appropriate, not maximal,
pattern use.
The classic LLD problems
🚨 Practice these — they recur constantly:
Parking lot, elevator system, vending machine, chess/tic-tac-toe, a deck of cards, an ATM, a library
management system, a rate limiter (LLD version), an in-memory key-value store, Snake and Ladders, a
logging framework, a notification system (LLD), an online food ordering system.
They share a shape: entities, state, and an operation that must be extensible. Practicing a few makes
the rest pattern-match.
What distinguishes a strong LLD answer
🚨 Beyond a working class diagram:
- Extensibility — the design handles “now add X” cleanly. The #1 signal.
- Appropriate patterns — used where they fit, not everywhere.
- SOLID adherence — especially open/closed and dependency inversion.
- Encapsulation — data hidden, accessed through methods; no leaky internals.
- Composition over inheritance — 🚨 preferring
has-a to deep is-a hierarchies where appropriate
(deep inheritance is brittle).
- Clean interfaces — well-defined contracts between classes.
- Thinking about concurrency if relevant (two cars parking simultaneously — thread safety).
🚨 Interview traps
- Treating it like a distributed-systems round — no servers/scale; it’s classes.
- Not clarifying requirements — same as the other framework.
- A design that doesn’t extend — fails the “now add X” test (the main thing graded).
- Over-using patterns — forcing patterns where simple methods suffice.
- Violating open/closed — needing to edit existing classes to add a type.
- Deep inheritance hierarchies — prefer composition.
- Not knowing SOLID — LLD rounds probe it explicitly.
🎙️ Soundbites
- “Let me identify the core entities first — the nouns become classes: ParkingLot, Floor, Spot, Vehicle,
Ticket.”
- “I’ll make Vehicle an abstract base with Car, Bike, Truck subclasses, so adding a new type is a new
class — satisfying open/closed.”
- “Pricing goes behind a Strategy interface, so a new pricing model is a new class, not a change to
existing code.”
- “For ‘now add EV charging spots’ — my design handles it by adding a ChargingSpot subclass and a
strategy for charging, without touching the existing spot logic.”
- “I’d use composition here rather than deep inheritance — a Spot *has a ChargingCapability rather than
a deep is-a hierarchy, which stays flexible.”*
🛠️ Try it
1. Design the classic problems, timed. Parking lot, elevator, vending machine, chess — 30 minutes
each, producing a class diagram. Then apply the “now add X” test to each and see if your design
extends cleanly. The extension test is the real practice.
2. Refactor toward SOLID. Take a design that violates open/closed (a big if/switch on type) and
refactor it to use polymorphism/strategy so new types are new classes. Feel the difference — this is
what LLD interviews reward.
3. Apply one pattern deliberately. For each classic problem, identify where Strategy, Factory,
Observer, or State genuinely fits and apply it. Practice justifying why the pattern fits — not
using it, but explaining the fit.
4. Practice the extension curve. Have a partner give you a base problem, then add three requirements
one at a time (as a real interviewer does at minute 35). Practice extending gracefully each time —
this simulates the actual test.
Check yourself
1. How is the LLD round different from the distributed-systems design round?
They test entirely different skills and require separate preparation. The **distributed-systems round**
("Design Twitter") is about *architecture at scale* — servers, load balancers, databases, caches, queues,
sharding, replication, consistency, and the trade-offs of a system serving millions of users; it operates
at the level of components and data flows across machines. The **LLD (low-level/object-oriented design)
round** ("Design the classes for a parking lot") is about *code structure* — classes, their
responsibilities and relationships, interfaces, encapsulation, SOLID principles, and design patterns; it
has no servers, no scale, no distributed concerns, and operates at the level of objects within a single
program. The LLD round tests clean object-oriented design and whether your class structure extends
gracefully when requirements change, while the distributed round tests system architecture and scaling
judgment. They use different frameworks (identify entities/relationships/patterns vs
requirements/estimation/high-level-design/deep-dives), reward different knowledge (SOLID and GoF patterns
vs caching, sharding, and CAP), and produce different artifacts (a class diagram vs a boxes-and-arrows
architecture). LLD is especially common in India and Pakistan, at Amazon as part of the loop, and at
product companies, so candidates targeting those must prepare for it as a distinct skill — applying the
distributed-systems framework to an LLD question (talking about load balancers when asked for classes)
would completely miss the point of the round.
2. Why is "now add X" the most important test in an LLD interview?
Because it directly measures the thing LLD is actually about: whether your design is *extensible*, which
is the real goal of good object-oriented design. A working class diagram that solves the stated problem is
necessary but not sufficient — anyone can model a parking lot as it's initially described. The test of
*quality* is what happens when the requirements change, because real software constantly changes, and a
design's value lies in how gracefully it accommodates change. So around minute 35, the interviewer adds a
requirement — "now support electric-vehicle charging spots," "now handle a new pricing model," "now add a
reservation system" — and observes whether your design absorbs it cleanly (add a new subclass, implement
an interface, plug in a strategy — *without touching existing code*) or requires rewriting existing
classes, editing switch statements, and unraveling assumptions. A design that extends cleanly demonstrates
that you applied the open/closed principle (open for extension, closed for modification), used
polymorphism and abstractions well, and structured responsibilities so that change is localized — which
is exactly the mark of good OOP. A design that requires surgery to extend reveals rigid, poorly-abstracted
code where every change ripples through the system. This is why the extension test is the primary signal:
it separates candidates who produced a superficially-correct model from those who designed for the change
that real software always brings, and it's the same reason SOLID (especially open/closed) is what these
rounds probe — SOLID principles exist precisely to make code extensible, and the "now add X" test is how
the interviewer checks whether you achieved that.
3. What is the Open/Closed Principle and why is it central to LLD?
The Open/Closed Principle (the "O" in SOLID) states that software entities should be *open for extension
but closed for modification* — meaning you should be able to add new behaviour by adding new code (new
classes, new implementations of an interface) rather than by modifying existing, working code. It's
central to LLD because it's the principle most directly tested by the "now add X" extension challenge, and
because it's what makes a design robust to change. The canonical violation is a big conditional on a type:
`if (vehicle.type == CAR) {...} else if (vehicle.type == BIKE) {...}` scattered through the code — adding
a new vehicle type requires editing every one of those conditionals (modification), which is error-prone
(you might miss one) and risky (you might break existing behaviour). The open/closed-compliant design uses
polymorphism instead: an abstract `Vehicle` base with `Car`, `Bike`, `Truck` subclasses each implementing
the type-specific behaviour, so adding an `ElectricCar` means writing a new subclass (extension) and
*touching nothing that already works* (closed to modification). Similarly, putting pricing behind a
`PricingStrategy` interface means a new pricing model is a new class implementing the interface, not an
edit to the pricing logic. Adhering to open/closed is what lets a design pass the extension test, which is
why it's the SOLID principle LLD interviews probe most heavily, and articulating it explicitly ("I'll make
this a Strategy so we satisfy open/closed — new models are new classes, not changes to existing code") is a
strong signal that you understand not just *how* to structure the classes but *why*, connecting your design
decisions to the principle that justifies them.
4. Why is over-using design patterns a negative signal in LLD?
Because it's the LLD equivalent of over-engineering — forcing complexity where simplicity would suffice —
and it signals poor judgment rather than sophistication, exactly as proposing sharding and microservices
for a small system does in the distributed-systems round. Design patterns are tools that solve specific
problems (Strategy for swappable algorithms, Observer for change notification, Factory for object
creation), and they add value *only when the problem they solve is actually present*. Applying a pattern
where a simple method or a plain class would do adds unnecessary abstraction, indirection, and complexity
— more classes, more interfaces, more moving parts — that makes the code harder to understand and maintain
without any compensating benefit. A candidate who wraps everything in a Factory, adds a Singleton
everywhere, and forces a Strategy pattern onto a decision that's just an `if` statement demonstrates that
they've memorized patterns as things to *display* rather than understanding them as solutions to apply
*when appropriate* — which reveals a lack of the judgment that distinguishes good engineers, who know that
the goal is clean, appropriate design, not maximal pattern usage. The interviewer wants to see patterns
applied where they genuinely fit and *justified by the fit* ("I'll use Strategy here because we have
multiple pricing algorithms that need to be swappable"), not patterns sprinkled everywhere to prove
familiarity. Just as the distributed round rewards choosing the simplest architecture that meets the
requirements, the LLD round rewards the simplest class structure that solves the problem and extends
cleanly — reaching for a pattern should be driven by a real need for what it provides, and using one
gratuitously is a mark against you, not for you.
5. Why prefer composition over inheritance in LLD, and when is inheritance still appropriate?
Composition (a class *has-a* reference to another object that provides some capability) is generally
preferred over deep inheritance (a class *is-a* subtype in a multi-level hierarchy) because deep
inheritance hierarchies are brittle and inflexible. Inheritance creates tight coupling between a subclass
and its base — changes to the base ripple to all subclasses, subclasses inherit everything whether they
need it or not, and the hierarchy is fixed at compile time. It also handles combinations poorly: if a
parking spot can be compact-or-large *and* charging-or-non-charging *and* covered-or-uncovered, modeling
every combination as a subclass produces a combinatorial explosion of classes (CompactChargingCoveredSpot,
LargeNonChargingUncoveredSpot...). Composition avoids this: a `Spot` *has* a `SizeCapability`, a
`ChargingCapability`, and a `CoveringCapability`, so capabilities combine freely and can even change at
runtime, and adding a new capability doesn't disturb the existing structure — which keeps the design
flexible and extensible (satisfying open/closed). This is why "favor composition over inheritance" is a
core OOP guideline and demonstrating it is a strong LLD signal. Inheritance is still appropriate, though,
when there's a genuine, stable *is-a* relationship with shared behaviour and a true type hierarchy — a
`Car` genuinely *is a* `Vehicle` and shares the vehicle contract, so an abstract `Vehicle` base with
concrete subclasses is the right model for representing vehicle *types* (and it satisfies Liskov
substitution, since any subclass works wherever the base is expected). The principle isn't "never use
inheritance" but "use inheritance for true type hierarchies with substitutable subtypes, and use
composition for combining capabilities or behaviours" — reaching for shallow, purposeful inheritance where
the is-a relationship is real, and composition where you'd otherwise create deep or combinatorial
hierarchies. Recognizing which tool fits — and defaulting to composition when in doubt — is the judgment
LLD interviews reward.
Further reading