Episode 9 — System Design / 9.1 — LLD Foundations
9.1.a — What Is Low-Level Design (LLD)?
In one sentence: Low-Level Design is the process of taking a high-level system architecture and breaking it down into classes, interfaces, methods, and their relationships — the code-level blueprint of a system.
Table of Contents
- 1. Defining Low-Level Design
- 2. LLD vs HLD — Comparison
- 3. When LLD Matters
- 4. LLD in Interviews vs Production
- 5. Components of Good LLD
- 6. The LLD Mindset
- 7. A Simple Example — Library System
- 8. Key Takeaways
1. Defining Low-Level Design
High-Level Design (HLD) answers: "What services, databases, and infrastructure do we need?"
Low-Level Design (LLD) answers: "Inside each service, what classes exist, what do they do, and how do they interact?"
┌─────────────────────────────────────────────────────────────────────┐
│ FROM REQUIREMENTS TO CODE │
│ │
│ Requirements │
│ │ │
│ ▼ │
│ High-Level Design (HLD) │
│ │ "We need a User Service, Order Service, Payment Gateway" │
│ ▼ │
│ Low-Level Design (LLD) │
│ │ "User Service has: User, UserRepository, AuthService, │
│ │ UserController classes with these methods..." │
│ ▼ │
│ Implementation (Code) │
│ "Actually writing the TypeScript / JavaScript files" │
└─────────────────────────────────────────────────────────────────────┘
LLD is NOT about writing every line of code. It is about designing the structure that the code will follow. Think of it as an architect's blueprint — it tells you where every room goes, but it is not the actual bricks and mortar.
2. LLD vs HLD — Comparison
| Dimension | High-Level Design (HLD) | Low-Level Design (LLD) |
|---|---|---|
| Scope | Entire system / multiple services | Single service or module |
| Audience | Architects, tech leads, stakeholders | Developers, senior engineers |
| Artifacts | Architecture diagrams, data-flow diagrams | Class diagrams, sequence diagrams, API contracts |
| Decisions | Which services? Which databases? Which protocols? | Which classes? Which methods? Which patterns? |
| Abstraction | Very high — boxes and arrows | Low — classes, interfaces, method signatures |
| Scale concern | Distributed systems, load balancing, replication | Single-process structure, code extensibility |
| Tools | System architecture tools, cloud diagrams | UML tools, code editors |
| Example question | "Design the architecture for Twitter" | "Design the classes for a parking lot system" |
| Time horizon | Hard to change later (infrastructure baked in) | Easier to refactor (but costly if done badly) |
Visual Comparison
HLD View: LLD View (zoomed into User Service):
┌──────────┐ HTTP ┌──────────┐ ┌─────────────────────────────────┐
│ Client │ ───────► │ API GW │ │ User Service │
└──────────┘ └────┬─────┘ │ │
│ │ ┌──────────────┐ │
┌────▼─────┐ │ │ UserController│ │
│ User │ │ └──────┬───────┘ │
│ Service │ │ │ │
└────┬─────┘ │ ┌──────▼───────┐ │
│ │ │ UserService │ │
┌────▼─────┐ │ └──────┬───────┘ │
│ DB │ │ │ │
└──────────┘ │ ┌──────▼───────┐ │
│ │UserRepository│ │
│ └──────────────┘ │
└─────────────────────────────────┘
3. When LLD Matters
3.1 In Software Development
| Scenario | Why LLD Helps |
|---|---|
| New feature development | Prevents haphazard code growth; establishes structure before typing |
| Team collaboration | Multiple devs can work on different classes simultaneously without conflicts |
| Code reviews | Reviewers can check if the design follows established patterns |
| Refactoring | A clear class structure makes safe refactoring possible |
| Testing | Well-separated classes with clear interfaces are far easier to unit-test |
| Onboarding | New team members understand the codebase through class diagrams faster than reading raw code |
3.2 When LLD Is Overkill
Not every piece of code needs a formal LLD. A quick script, a prototype, or a one-off utility can be written directly. LLD shines when:
- The module will be maintained for months or years.
- Multiple developers will work on it.
- The problem has multiple entities with complex interactions.
- You need to extend the system with new features regularly.
4. LLD in Interviews vs Production
| Aspect | Interview LLD | Production LLD |
|---|---|---|
| Time | 30–45 minutes | Days to weeks of iteration |
| Depth | Core classes + key methods | Every class, every edge case, error handling |
| Diagrams | Hand-drawn on whiteboard or ASCII | Formal UML in tools (PlantUML, Lucidchart) |
| Patterns | Name 2–3 patterns you would apply | Full pattern implementation with tests |
| Code | Pseudocode or skeleton classes | Fully compilable, tested, reviewed code |
| Trade-offs | Discuss verbally | Documented in ADRs (Architecture Decision Records) |
| Focus | Demonstrate thinking process | Deliver working, maintainable software |
Interview tip: Interviewers care more about your thought process (how you identify entities, define relationships, handle edge cases) than whether your code compiles perfectly.
What Interviewers Are Evaluating
┌────────────────────────────────────────────────────────────────┐
│ LLD INTERVIEW SCORING RUBRIC │
│ │
│ 1. Requirement Gathering — Did you ask clarifying Qs? │
│ 2. Entity Identification — Did you find the right classes?│
│ 3. Class Design — Are responsibilities clear? │
│ 4. Relationships — Correct use of composition, │
│ inheritance, etc.? │
│ 5. Method Signatures — Sensible parameters & returns? │
│ 6. Design Patterns — Applied where appropriate? │
│ 7. Extensibility — Can new features be added? │
│ 8. Edge Cases — Did you think about failures? │
└────────────────────────────────────────────────────────────────┘
5. Components of Good LLD
5.1 Classes
A class is a blueprint for creating objects. In LLD, you decide:
- What classes exist (nouns in the problem — User, Order, Payment).
- What data each class holds (attributes/properties).
- What each class does (methods/behaviors).
// A well-designed class has a single, clear responsibility
class User {
private id: string;
private name: string;
private email: string;
private createdAt: Date;
constructor(id: string, name: string, email: string) {
this.id = id;
this.name = name;
this.email = email;
this.createdAt = new Date();
}
getName(): string {
return this.name;
}
updateEmail(newEmail: string): void {
if (!this.isValidEmail(newEmail)) {
throw new Error("Invalid email format");
}
this.email = newEmail;
}
private isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
}
5.2 Interfaces
An interface defines a contract — what methods a class must provide, without dictating how. This enables loose coupling.
// Interface defines WHAT, not HOW
interface PaymentProcessor {
processPayment(amount: number, currency: string): PaymentResult;
refund(transactionId: string): RefundResult;
getTransactionStatus(transactionId: string): TransactionStatus;
}
// Multiple implementations can fulfill the same interface
class StripeProcessor implements PaymentProcessor {
processPayment(amount: number, currency: string): PaymentResult {
// Stripe-specific implementation
return { success: true, transactionId: "stripe_txn_123" };
}
refund(transactionId: string): RefundResult {
// Stripe-specific refund logic
return { success: true };
}
getTransactionStatus(transactionId: string): TransactionStatus {
return TransactionStatus.COMPLETED;
}
}
class PayPalProcessor implements PaymentProcessor {
processPayment(amount: number, currency: string): PaymentResult {
// PayPal-specific implementation
return { success: true, transactionId: "pp_txn_456" };
}
refund(transactionId: string): RefundResult {
return { success: true };
}
getTransactionStatus(transactionId: string): TransactionStatus {
return TransactionStatus.COMPLETED;
}
}
5.3 Relationships
How classes connect to each other. The main types are:
┌──────────────────────────────────────────────────────────────────┐
│ CLASS RELATIONSHIP TYPES │
│ │
│ Association ── "knows about" (Teacher ↔ Student) │
│ Aggregation ── "has a" (weak) (Department ◇── Employee) │
│ Composition ── "owns a" (strong) (House ◆── Room) │
│ Dependency ── "uses temporarily"(Order ┄┄► Logger) │
│ Inheritance ── "is a" (Dog ──▷ Animal) │
│ Implementation ── "can do" (Circle ┄┄▷ Shape) │
└──────────────────────────────────────────────────────────────────┘
Each of these is covered in depth in 9.1.c — Class Relationships.
5.4 Methods
Methods define behavior. Good LLD methods have:
| Quality | Description | Example |
|---|---|---|
| Clear name | Verb + noun describing what it does | calculateTotal(), not doStuff() |
| Single purpose | Does one thing well | validateEmail() not validateAndSave() |
| Defined parameters | Typed, minimal, necessary inputs | (userId: string) not (data: any) |
| Clear return type | Caller knows what to expect | Promise<User> not any |
| Error handling | Fails gracefully with clear errors | Throws UserNotFoundError, not generic Error |
// Bad: vague, does too much, unclear return
function handle(data: any): any {
// ...mystery code...
}
// Good: clear name, typed, single purpose
function findUserById(userId: string): User | null {
const user = this.userRepository.findById(userId);
return user ?? null;
}
5.5 Design Patterns
Reusable solutions to common design problems. You do not need to memorize all of them for LLD, but knowing the key ones is essential:
| Category | Patterns | When to Use |
|---|---|---|
| Creational | Singleton, Factory, Builder | When object creation is complex or needs control |
| Structural | Adapter, Decorator, Facade | When you need to compose objects or simplify interfaces |
| Behavioral | Strategy, Observer, State | When behavior changes based on context or events |
Design patterns are covered in detail in Episodes 9.3, 9.4, and 9.5.
6. The LLD Mindset
Good LLD practitioners think in terms of:
6.1 Separation of Concerns
Each class handles one responsibility. If you cannot describe a class in one sentence, it probably does too much.
Bad: UserManager — handles login, registration, profile updates,
email sending, password hashing, session management
Good: AuthService — handles login/logout
UserService — handles profile CRUD
EmailService — handles sending emails
PasswordHasher — handles password hashing
SessionManager — handles session lifecycle
6.2 Open for Extension, Closed for Modification
New features should be added by creating new classes, not modifying existing ones.
// Instead of modifying NotificationService every time:
// BAD approach:
class NotificationService {
notify(user: User, type: string) {
if (type === "email") { /* send email */ }
else if (type === "sms") { /* send sms */ }
else if (type === "push") { /* send push */ }
// Every new channel = modify this class
}
}
// GOOD approach: use an interface
interface NotificationChannel {
send(user: User, message: string): void;
}
class EmailChannel implements NotificationChannel {
send(user: User, message: string): void { /* email logic */ }
}
class SMSChannel implements NotificationChannel {
send(user: User, message: string): void { /* sms logic */ }
}
// Adding a new channel = add a new class, no existing code changes
class SlackChannel implements NotificationChannel {
send(user: User, message: string): void { /* slack logic */ }
}
6.3 Depend on Abstractions, Not Concretions
Code should depend on interfaces, not specific classes.
// BAD: OrderService directly depends on StripeProcessor
class OrderService {
private stripe = new StripeProcessor(); // tightly coupled
}
// GOOD: OrderService depends on the PaymentProcessor interface
class OrderService {
private paymentProcessor: PaymentProcessor;
constructor(paymentProcessor: PaymentProcessor) {
this.paymentProcessor = paymentProcessor; // loosely coupled
}
}
7. A Simple Example — Library System
Let us walk through a quick LLD sketch for a simple library system.
Step 1: Identify Entities (Nouns)
From the requirements: "A library has books. Members can borrow and return books. Each book has a title, author, and ISBN. Members have a membership ID."
Entities: Book, Member, Library, BorrowRecord
Step 2: Define Classes
class Book {
private isbn: string;
private title: string;
private author: string;
private isAvailable: boolean;
constructor(isbn: string, title: string, author: string) {
this.isbn = isbn;
this.title = title;
this.author = author;
this.isAvailable = true;
}
getIsbn(): string { return this.isbn; }
getTitle(): string { return this.title; }
markBorrowed(): void { this.isAvailable = false; }
markReturned(): void { this.isAvailable = true; }
checkAvailability(): boolean { return this.isAvailable; }
}
class Member {
private memberId: string;
private name: string;
private borrowedBooks: Book[];
constructor(memberId: string, name: string) {
this.memberId = memberId;
this.name = name;
this.borrowedBooks = [];
}
getMemberId(): string { return this.memberId; }
getBorrowedBooks(): Book[] { return [...this.borrowedBooks]; }
borrowBook(book: Book): void {
this.borrowedBooks.push(book);
}
returnBook(book: Book): void {
this.borrowedBooks = this.borrowedBooks.filter(
b => b.getIsbn() !== book.getIsbn()
);
}
}
class BorrowRecord {
private member: Member;
private book: Book;
private borrowDate: Date;
private returnDate: Date | null;
constructor(member: Member, book: Book) {
this.member = member;
this.book = book;
this.borrowDate = new Date();
this.returnDate = null;
}
markReturned(): void {
this.returnDate = new Date();
}
}
class Library {
private books: Map<string, Book>;
private members: Map<string, Member>;
private records: BorrowRecord[];
constructor() {
this.books = new Map();
this.members = new Map();
this.records = [];
}
addBook(book: Book): void {
this.books.set(book.getIsbn(), book);
}
registerMember(member: Member): void {
this.members.set(member.getMemberId(), member);
}
borrowBook(memberId: string, isbn: string): BorrowRecord {
const member = this.members.get(memberId);
const book = this.books.get(isbn);
if (!member) throw new Error("Member not found");
if (!book) throw new Error("Book not found");
if (!book.checkAvailability()) throw new Error("Book not available");
book.markBorrowed();
member.borrowBook(book);
const record = new BorrowRecord(member, book);
this.records.push(record);
return record;
}
returnBook(memberId: string, isbn: string): void {
const member = this.members.get(memberId);
const book = this.books.get(isbn);
if (!member) throw new Error("Member not found");
if (!book) throw new Error("Book not found");
book.markReturned();
member.returnBook(book);
}
}
Step 3: Visualize Relationships
┌──────────┐ ┌──────────────┐
│ Library │◆────────│ Book │ Library OWNS books (composition)
│ │ └──────────────┘
│ │◆────────┌──────────────┐
│ │ │ Member │ Library OWNS members (composition)
│ │ └──────┬───────┘
│ │ │
│ │◆────────┌──────▼───────┐
│ │ │ BorrowRecord │ Library OWNS records (composition)
└──────────┘ │ │
│ member ─────┤ Record REFERENCES member (association)
│ book ─────┤ Record REFERENCES book (association)
└──────────────┘
8. Key Takeaways
- LLD bridges the gap between architecture diagrams (HLD) and actual code.
- LLD is about structure — which classes, which methods, which relationships — not about writing every line.
- Good LLD separates concerns, depends on abstractions, and makes extension easy.
- Interviews evaluate your thinking process — entity identification, relationship choices, and trade-off discussion matter more than syntax.
- Not every project needs formal LLD — use it when the system is complex enough to warrant structured planning.
Explain-It Challenge
Can you explain to a colleague: "What is Low-Level Design and how does it differ from High-Level Design?" If you can do it in under 60 seconds with a clear example, you have mastered this topic.
Next → 9.1.b — OOP Fundamentals