system-design

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):

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:

🎙️ “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:


🚨 Interview traps


🎙️ Soundbites


🛠️ 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