The blueprint for scale: LEGO city analogy
Imagine you are asked to build a physical LEGO city. If you dump all the bricks into one giant container, you will struggle to find parts, change the design, or collaborate with others.
In software engineering, Low-Level Design (LLD) (also called Object-Oriented Design) is the craft of organizing your code like modular LEGO sets:
- Classes/Objects are the bricks. Each brick has a specific color, shape, and connection interface.
- Interfaces are the standard connector shapes (studs and tubes). Any brick with 2x4 studs fits onto any 2x4 slot, regardless of what that brick is made of.
- UML Diagrams are the instruction manuals showing how bricks snap together.
- SOLID Principles are the rules of architectural durability, ensuring you can swap a building's roof without collapsing the entire structure.
The four pillars
- Encapsulation — hide internal state; expose behavior. Invariants are protected because state only changes through methods.
- Abstraction — model the essentials, hide the mechanism. Callers depend on what, not how.
- Inheritance — share/extend behavior via an "is-a" relationship. Powerful but over-used; prefer composition ("has-a") for flexibility.
- Polymorphism — one interface, many implementations; the caller doesn't branch on type.
SOLID
| Principle | The test | |
|---|---|---|
| S | Single Responsibility | "One reason to change." A class does one job. |
| O | Open/Closed | Open to extension, closed to modification — add a class, don't edit a switch. |
| L | Liskov Substitution | A subtype must be usable anywhere its base is, without surprises. |
| I | Interface Segregation | Many small interfaces beat one fat one; don't force unused methods. |
| D | Dependency Inversion | Depend on abstractions, not concretions. |
Dependency Inversion in one example
The whole principle is one arrow flip. Before: high-level policy points down at a concrete detail. After: both point at an abstraction — the detail now depends on the interface, not the other way around.
// ❌ High-level policy welded to a concrete detail
class OrderService {
private email = new SmtpEmailSender(); // can't swap, can't test
confirm(o: Order) { this.email.send(o.userEmail, "Confirmed"); }
}
// ✅ Depend on an abstraction; inject the detail
interface Notifier { send(to: string, msg: string): void; }
class OrderService {
constructor(private notifier: Notifier) {} // inverted
confirm(o: Order) { this.notifier.send(o.userEmail, "Confirmed"); }
}
// Now SMTP, SMS, or a fake for tests all satisfy Notifier — Open/Closed too.
The goal is code that's easy to change and test. Quote the principle, then show
the smell it removes (a giant switch, an untestable new, a subtype that
throws on a base method). Interviewers want the judgment, not the acronym.
Composition over inheritance
Deep inheritance trees are rigid (a change high up ripples down) and force a single axis of variation. Composition lets you assemble behavior from small parts and swap them at runtime — it's why Strategy, Decorator and Dependency Injection all favor "has-a".
The left side pays 2^N classes for N independent choices; the right side pays one interface per axis of variation. "What varies here?" is the question that leads you to every pattern on the design patterns page.
The SDE LLD Interview Protocol
LLD interviews are not about jumping straight into typing code; they are about structured problem-solving. Follow this 6-step whiteboard recipe:
- Clarify Scope & Constraints (5 mins):
- Ask functional questions: "Single or multiple levels? Which vehicle sizes?"
- Define non-functional constraints: "Do we need thread safety? Are we persist-to-database or in-memory?"
- Establish invariants: "A parking spot holds at most one vehicle."
- Noun/Verb Extraction (5 mins):
- Nouns (Classes/Attributes): Read the requirements and extract the key actors (e.g.,
ParkingLot,Level,ParkingSpot,Vehicle,Ticket). - Verbs (Methods): Map actions to responsibilities (e.g.,
parkVehicle(v),findSpot(v),canFit(v)). Avoid creating a "God Class" that holds all logic.
- Nouns (Classes/Attributes): Read the requirements and extract the key actors (e.g.,
- Draft the Class Diagram (UML) (10 mins):
- Map entities and their relationships (Association, Aggregation, Composition, Generalization, Realization).
- Cardinalities: "One ParkingLot has many Levels (1 to *)."
- Identify What Varies (Design Patterns) (5 mins):
- "Fee calculation varies → Strategy pattern."
- "Elevator behavior changes per state → State pattern."
- Confront Concurrency (5 mins):
- Ask: "What happens if two users request the same resource simultaneously?"
- Identify critical sections (find-and-reserve) and explain lock choices (mutexes, concurrent data structures).
- Code the Core Interfaces & Flow (15 mins):
- Write clean, modular, typed code. Define interfaces first, then implement the key flow.
UML Relationship Guide (with multi-language code)
UML (Unified Modeling Language) is the whiteboard language of LLD. To read and draw class diagrams, you must understand the five primary relationships, how they are drawn in Mermaid, and how they translate to code:
1. Generalization / Inheritance (<|-- or is-a)
- Meaning: A child class inherits state and behavior from a parent class.
- Mermaid:
Parent <|-- Child - Code:
# Python
class Vehicle: pass
class Car(Vehicle): pass
// Java
class Vehicle {}
class Car extends Vehicle {}
// C++
class Vehicle {};
class Car : public Vehicle {};
2. Realization / Implementation (..|> or is-a)
- Meaning: A concrete class implements the contract defined by an interface or abstract class.
- Mermaid:
Interface <.. Concrete - Code:
# Python (using Abstract Base Class - ABC)
from abc import ABC, abstractmethod
class Notifier(ABC):
@abstractmethod
def send(self, msg): pass
class EmailNotifier(Notifier):
def send(self, msg): print(msg)
// Java
interface Notifier { void send(String msg); }
class EmailNotifier implements Notifier {
public void send(String msg) { System.out.println(msg); }
}
// C++ (Pure Virtual functions)
class Notifier {
public:
virtual void send(std::string msg) = 0;
};
class EmailNotifier : public Notifier {
public:
void send(std::string msg) override { std::cout << msg; }
};
3. Association (--> or has-a)
- Meaning: An independent relationship where one class references another, but they have independent lifecycles.
- Mermaid:
A --> B - Code:
# Python
class Passenger: pass
class Car:
def __init__(self, passenger):
self.passenger = passenger # holds reference, but passenger lives independently
// Java
class Passenger {}
class Car {
private Passenger passenger;
public Car(Passenger p) { this.passenger = p; }
}
// C++
class Passenger {};
class Car {
private:
Passenger* passenger; // raw or smart pointer
public:
Car(Passenger* p) : passenger(p) {}
};
4. Aggregation (o-- or part-of)
- Meaning: A loose ownership relationship ("whole/part"). The child is part of the parent, but can survive if the parent is destroyed.
- Mermaid:
Parent o-- Child - Code:
# Python
class Wheel: pass
class Car:
def __init__(self, wheels):
self.wheels = wheels # wheels can exist outside the car (passed in)
// Java
class Wheel {}
class Car {
private List<Wheel> wheels;
public Car(List<Wheel> wheels) { this.wheels = wheels; }
}
// C++
class Wheel {};
class Car {
private:
std::vector<Wheel*> wheels;
public:
Car(std::vector<Wheel*> w) : wheels(w) {}
};
5. Composition (*-- or part-of with lifespans bound)
- Meaning: A strong ownership relationship. The child is created inside the parent and cannot exist without it; if the parent is destroyed, the child is destroyed too.
- Mermaid:
Parent *-- Child - Code:
# Python
class Engine: pass
class Car:
def __init__(self):
self.engine = Engine() # Engine created inside Car, bound to its lifespan
// Java
class Engine {}
class Car {
private Engine engine;
public Car() {
this.engine = new Engine(); // bound lifecycle
}
}
// C++
class Engine {};
class Car {
private:
Engine engine; // instanced directly inside Car object
public:
Car() : engine() {}
};
Think it through
Reason through how to map raw requirement text into clean classes and relationships before revealing each stage.
PROBLEMDesign the classes for a coffee machine: users choose a beverage, insert coins, and the machine dispenses the coffee. If ingredients are low, the machine alerts the user.
- 1
Extract the nouns (Classes)
“Find the candidate nouns in the prompt, and filter them into core classes vs attributes.”
- 2
Extract the verbs (Methods)
“Map the actions to their responsible classes. Who owns dispense()? Who tracks ingredient levels?”
unlocks after the stage above - 3
Determine the relationships
“What is the relationship between: (1) CoffeeMachine and Ingredient, (2) CoffeeMachine and Beverage, (3) Beverage and Espresso?”
unlocks after the stage above - 4
Mermaid Class Diagram
“Express these classes and relationships in Mermaid format.”
unlocks after the stage above
Check yourself
1. In UML, what is the difference between Aggregation (o--) and Composition (*--)?
2. The Open/Closed Principle (OCP) is best satisfied by using which of the following OOP techniques?
3. Why does the Dependency Inversion Principle (DIP) advocate depending on abstractions rather than concretions?
Practice — level up
SOLID becomes muscle memory by modeling small things cleanly: one responsibility per class, behavior composed rather than inherited, an invariant held behind a tidy interface. These "design a structure" drills train exactly that.
Climb in order — every rung assumes the one above it. Solve on LeetCode, then tick it here; progress is saved on this device.
Warm-up — one class, one invariant
- Min StackEasy
Hold an extra invariant (the running min) behind push/pop — encapsulation, Single Responsibility.
Core — an interface that hides its guts
Callers see operations, never the internals.- Design HashMapEasy
A clean put/get/remove contract over hidden buckets — abstraction and information hiding.
- LRU CacheMedium
Compose a map + linked list behind get/put — composition over inheritance, done right.
Stretch — pick internals the API implies
- Design Browser HistoryMedium
Choose internals that keep every operation cheap — Open/Closed against future operations.