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

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

DimensionHigh-Level Design (HLD)Low-Level Design (LLD)
ScopeEntire system / multiple servicesSingle service or module
AudienceArchitects, tech leads, stakeholdersDevelopers, senior engineers
ArtifactsArchitecture diagrams, data-flow diagramsClass diagrams, sequence diagrams, API contracts
DecisionsWhich services? Which databases? Which protocols?Which classes? Which methods? Which patterns?
AbstractionVery high — boxes and arrowsLow — classes, interfaces, method signatures
Scale concernDistributed systems, load balancing, replicationSingle-process structure, code extensibility
ToolsSystem architecture tools, cloud diagramsUML tools, code editors
Example question"Design the architecture for Twitter""Design the classes for a parking lot system"
Time horizonHard 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

ScenarioWhy LLD Helps
New feature developmentPrevents haphazard code growth; establishes structure before typing
Team collaborationMultiple devs can work on different classes simultaneously without conflicts
Code reviewsReviewers can check if the design follows established patterns
RefactoringA clear class structure makes safe refactoring possible
TestingWell-separated classes with clear interfaces are far easier to unit-test
OnboardingNew 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

AspectInterview LLDProduction LLD
Time30–45 minutesDays to weeks of iteration
DepthCore classes + key methodsEvery class, every edge case, error handling
DiagramsHand-drawn on whiteboard or ASCIIFormal UML in tools (PlantUML, Lucidchart)
PatternsName 2–3 patterns you would applyFull pattern implementation with tests
CodePseudocode or skeleton classesFully compilable, tested, reviewed code
Trade-offsDiscuss verballyDocumented in ADRs (Architecture Decision Records)
FocusDemonstrate thinking processDeliver 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:

QualityDescriptionExample
Clear nameVerb + noun describing what it doescalculateTotal(), not doStuff()
Single purposeDoes one thing wellvalidateEmail() not validateAndSave()
Defined parametersTyped, minimal, necessary inputs(userId: string) not (data: any)
Clear return typeCaller knows what to expectPromise<User> not any
Error handlingFails gracefully with clear errorsThrows 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:

CategoryPatternsWhen to Use
CreationalSingleton, Factory, BuilderWhen object creation is complex or needs control
StructuralAdapter, Decorator, FacadeWhen you need to compose objects or simplify interfaces
BehavioralStrategy, Observer, StateWhen 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

  1. LLD bridges the gap between architecture diagrams (HLD) and actual code.
  2. LLD is about structure — which classes, which methods, which relationships — not about writing every line.
  3. Good LLD separates concerns, depends on abstractions, and makes extension easy.
  4. Interviews evaluate your thinking process — entity identification, relationship choices, and trade-off discussion matter more than syntax.
  5. 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