systemdesigndoc
LLD

What is Low-Level Design?

2026-03-22javalldfundamentalsoop

What is Low-Level Design?

Low-Level Design is the practice of structuring code at the class, interface, and method level so that it is readable, extensible, testable, and maintainable. It's the bridge between a vague requirement ("build a parking lot system") and actual working code.

Every line of code you write is an LLD decision. Choosing between inheritance and composition, deciding which class owns which responsibility, figuring out how objects collaborate — all of this is LLD.

mermaid
graph LR
    REQ["Requirements<br/>'Build a parking lot'"]
    HLD["High-Level Design<br/>Services, APIs,<br/>databases, queues"]
    LLD["Low-Level Design<br/>Classes, interfaces,<br/>methods, patterns"]
    CODE["Code<br/>Actual Java<br/>implementation"]
 
    REQ --> HLD --> LLD --> CODE
 
    style REQ fill:#f1f5f9,stroke:#94a3b8
    style HLD fill:#dbeafe,stroke:#3b82f6
    style LLD fill:#dcfce7,stroke:#22c55e
    style CODE fill:#fef3c7,stroke:#f59e0b
Where LLD sits in the software design spectrum

LLD vs HLD — Understanding the Scope

This is the most common confusion. Both are "design," but they operate at completely different zoom levels.

mermaid
graph TB
    subgraph HLD["HLD: Satellite View"]
        direction LR
        CLIENT["Client App"]
        LB["Load Balancer"]
        API["API Server"]
        CACHE["Redis Cache"]
        DB["PostgreSQL"]
        QUEUE["Message Queue"]
        WORKER["Worker Service"]
 
        CLIENT --> LB --> API
        API --> CACHE
        API --> DB
        API --> QUEUE
        QUEUE --> WORKER
    end
 
    subgraph LLD["LLD: Zooming into one component"]
        direction TB
        CTRL["ParkingController"]
        SVC["ParkingService"]
        STRAT["ParkingStrategy<br/>«interface»"]
        NEAR["NearestFirstStrategy"]
        SPREAD["SpreadOutStrategy"]
        LOT["ParkingLot"]
        FLOOR["ParkingFloor"]
        SPOT["ParkingSpot"]
        VEHICLE["Vehicle<br/>«abstract»"]
        CAR["Car"]
        TRUCK["Truck"]
        BIKE["Bike"]
 
        CTRL --> SVC
        SVC --> STRAT
        STRAT -.-> NEAR
        STRAT -.-> SPREAD
        SVC --> LOT
        LOT --> FLOOR
        FLOOR --> SPOT
        SPOT --> VEHICLE
        VEHICLE -.-> CAR
        VEHICLE -.-> TRUCK
        VEHICLE -.-> BIKE
    end
 
    HLD -.->|"Zoom into<br/>API Server"| LLD
 
    style HLD fill:#dbeafe,stroke:#3b82f6
    style LLD fill:#dcfce7,stroke:#22c55e
HLD vs LLD — the zoom metaphor
AspectHLDLLD
Question it answersWhat components do we need? How do they communicate?How do we implement one component internally?
ArtifactsArchitecture diagrams, service maps, data flow diagramsClass diagrams, sequence diagrams, code
DecisionsWhich database? REST or gRPC? Monolith or microservices?Which design pattern? Inheritance or composition? Who owns this responsibility?
ScaleSystem-wide (multiple services)Single service or module
Interview format45 min whiteboard: "Design Instagram"45 min coding: "Design a parking lot in Java"

UML class diagram — the standard notation for expressing LLD visually

Why LLD Matters — The Real Cost of Bad Design

Bad LLD isn't just ugly code. It has concrete, measurable consequences:

mermaid
graph TD
    subgraph BAD["Bad LLD"]
        B1["New feature request"]
        B2["Read 5 classes to<br/>understand the flow"]
        B3["Modify 3 existing<br/>classes to add feature"]
        B4["Break 2 existing<br/>tests"]
        B5["Fix tests, introduce<br/>new bug"]
        B6["Hotfix in production"]
 
        B1 --> B2 --> B3 --> B4 --> B5 --> B6
    end
 
    subgraph GOOD["Good LLD"]
        G1["New feature request"]
        G2["Read 1 interface to<br/>understand the contract"]
        G3["Create 1 new class<br/>implementing the interface"]
        G4["All existing tests<br/>still pass"]
        G5["Ship to production"]
 
        G1 --> G2 --> G3 --> G4 --> G5
    end
 
    style BAD fill:#fee2e2,stroke:#ef4444
    style GOOD fill:#dcfce7,stroke:#22c55e
The compounding cost of bad design over time

In interviews, LLD questions test exactly this: can you structure code so that adding a new feature is creating a new class, not modifying ten existing ones?

The Four Pillars of Good LLD

Every good low-level design exhibits four properties. These aren't abstract ideals — they're practical qualities you can test for.

1. Single Responsibility

Each class does one thing. If you can't describe what a class does in one sentence without using "and," it's doing too much.

java
// BAD: This class does everything
public class OrderService {
    public Order createOrder(Cart cart) { /* ... */ }
    public void sendConfirmationEmail(Order order) { /* ... */ }
    public void updateInventory(Order order) { /* ... */ }
    public void processPayment(Order order) { /* ... */ }
    public PDF generateInvoice(Order order) { /* ... */ }
}
java
// GOOD: Each class has one job
public class OrderService {
    private final PaymentService paymentService;
    private final InventoryService inventoryService;
    private final NotificationService notificationService;
    private final InvoiceService invoiceService;
 
    public Order createOrder(Cart cart) {
        Order order = Order.from(cart);
        paymentService.process(order);
        inventoryService.reserve(order);
        notificationService.sendConfirmation(order);
        return order;
    }
}

2. Open for Extension, Closed for Modification

You should be able to add new behavior without changing existing code. This is the backbone of extensible design.

mermaid
classDiagram
    class PaymentProcessor {
        <<interface>>
        +process(order: Order): PaymentResult
    }
 
    class CreditCardProcessor {
        +process(order: Order): PaymentResult
    }
 
    class UPIProcessor {
        +process(order: Order): PaymentResult
    }
 
    class WalletProcessor {
        +process(order: Order): PaymentResult
    }
 
    class NetBankingProcessor {
        +process(order: Order): PaymentResult
    }
 
    PaymentProcessor <|.. CreditCardProcessor
    PaymentProcessor <|.. UPIProcessor
    PaymentProcessor <|.. WalletProcessor
    PaymentProcessor <|.. NetBankingProcessor : "NEW — no existing code changed"
 
    note for NetBankingProcessor "Adding this required ZERO\nchanges to existing classes"
Open-Closed Principle — adding a new payment method

3. Dependency Inversion

High-level modules should not depend on low-level modules. Both should depend on abstractions (interfaces).

java
// BAD: High-level class directly depends on low-level implementation
public class NotificationService {
    private final SmtpEmailSender emailSender = new SmtpEmailSender();
 
    public void notify(User user, String message) {
        emailSender.send(user.getEmail(), message);
    }
}
java
// GOOD: Both depend on an abstraction
public interface MessageSender {
    void send(String destination, String message);
}
 
public class SmtpEmailSender implements MessageSender {
    public void send(String destination, String message) {
        // SMTP implementation
    }
}
 
public class TwilioSmsSender implements MessageSender {
    public void send(String destination, String message) {
        // Twilio SMS implementation
    }
}
 
public class NotificationService {
    private final MessageSender sender;
 
    public NotificationService(MessageSender sender) {
        this.sender = sender; // injected — easy to swap, easy to test
    }
 
    public void notify(User user, String message) {
        sender.send(user.getContactInfo(), message);
    }
}

Observer pattern — a classic example of good LLD enabling loose coupling between components

4. Composition Over Inheritance

Inheritance creates tight coupling. Composition gives you flexibility to mix and match behaviors.

mermaid
graph TB
    subgraph Inheritance["❌ Inheritance — rigid hierarchy"]
        AN["Animal"]
        FLY_AN["FlyingAnimal"]
        SWIM_AN["SwimmingAnimal"]
        DUCK_BAD["Duck extends ???<br/>(it flies AND swims)"]
 
        AN --> FLY_AN
        AN --> SWIM_AN
        FLY_AN -.->|"Can't extend both!"| DUCK_BAD
        SWIM_AN -.->|"Can't extend both!"| DUCK_BAD
    end
 
    subgraph Composition["✅ Composition — flexible behaviors"]
        ANIMAL["Animal"]
        FLY_B["FlyBehavior<br/>«interface»"]
        SWIM_B["SwimBehavior<br/>«interface»"]
        DUCK["Duck<br/>has FlyBehavior<br/>has SwimBehavior"]
        PENGUIN["Penguin<br/>has SwimBehavior<br/>no FlyBehavior"]
 
        ANIMAL --> DUCK
        ANIMAL --> PENGUIN
        FLY_B -.-> DUCK
        SWIM_B -.-> DUCK
        SWIM_B -.-> PENGUIN
    end
 
    style Inheritance fill:#fee2e2,stroke:#ef4444
    style Composition fill:#dcfce7,stroke:#22c55e
Inheritance (rigid) vs Composition (flexible)

UML class diagram — attributes, methods, and relationships between classes

A Complete Example: Notification System

Let's walk through designing a notification system from scratch — the way you'd do it in an LLD interview.

Requirements

  • Send notifications via Email, SMS, and Push
  • Support adding new channels without modifying existing code
  • Support notification priorities (urgent, normal, low)
  • Retry failed notifications
  • Log all notification attempts

Step 1: Identify the Core Abstractions

mermaid
classDiagram
    class NotificationChannel {
        <<interface>>
        +send(notification: Notification): boolean
        +supports(type: ChannelType): boolean
    }
 
    class EmailChannel {
        -smtpClient: SmtpClient
        +send(notification: Notification): boolean
        +supports(type: ChannelType): boolean
    }
 
    class SmsChannel {
        -twilioClient: TwilioClient
        +send(notification: Notification): boolean
        +supports(type: ChannelType): boolean
    }
 
    class PushChannel {
        -fcmClient: FcmClient
        +send(notification: Notification): boolean
        +supports(type: ChannelType): boolean
    }
 
    class NotificationService {
        -channels: List~NotificationChannel~
        -retryPolicy: RetryPolicy
        -logger: NotificationLogger
        +notify(user: User, notification: Notification): void
    }
 
    class Notification {
        -id: String
        -message: String
        -priority: Priority
        -channelType: ChannelType
        -createdAt: Instant
    }
 
    class RetryPolicy {
        -maxRetries: int
        -backoffMs: long
        +shouldRetry(attempt: int, error: Exception): boolean
        +getDelay(attempt: int): long
    }
 
    class NotificationLogger {
        +logSuccess(notification: Notification, channel: String): void
        +logFailure(notification: Notification, channel: String, error: Exception): void
    }
 
    NotificationChannel <|.. EmailChannel
    NotificationChannel <|.. SmsChannel
    NotificationChannel <|.. PushChannel
    NotificationService --> NotificationChannel : uses
    NotificationService --> RetryPolicy : uses
    NotificationService --> NotificationLogger : uses
    NotificationService --> Notification : processes
Notification system class diagram

Step 2: Implement the Core Code

java
public enum Priority { URGENT, NORMAL, LOW }
public enum ChannelType { EMAIL, SMS, PUSH }
 
public record Notification(
    String id,
    String message,
    Priority priority,
    ChannelType channelType,
    Instant createdAt
) {
    public Notification(String message, Priority priority, ChannelType channelType) {
        this(UUID.randomUUID().toString(), message, priority, channelType, Instant.now());
    }
}
java
public interface NotificationChannel {
    boolean send(Notification notification, User user);
    boolean supports(ChannelType type);
}
 
public class EmailChannel implements NotificationChannel {
    private final SmtpClient smtpClient;
 
    public EmailChannel(SmtpClient smtpClient) {
        this.smtpClient = smtpClient;
    }
 
    @Override
    public boolean send(Notification notification, User user) {
        return smtpClient.send(user.getEmail(), notification.message());
    }
 
    @Override
    public boolean supports(ChannelType type) {
        return type == ChannelType.EMAIL;
    }
}
java
public class NotificationService {
    private final List<NotificationChannel> channels;
    private final RetryPolicy retryPolicy;
    private final NotificationLogger logger;
 
    public NotificationService(
            List<NotificationChannel> channels,
            RetryPolicy retryPolicy,
            NotificationLogger logger) {
        this.channels = channels;
        this.retryPolicy = retryPolicy;
        this.logger = logger;
    }
 
    public void notify(User user, Notification notification) {
        NotificationChannel channel = channels.stream()
            .filter(c -> c.supports(notification.channelType()))
            .findFirst()
            .orElseThrow(() -> new UnsupportedChannelException(notification.channelType()));
 
        int attempt = 0;
        while (true) {
            try {
                boolean sent = channel.send(notification, user);
                if (sent) {
                    logger.logSuccess(notification, channel.getClass().getSimpleName());
                    return;
                }
            } catch (Exception e) {
                if (!retryPolicy.shouldRetry(attempt, e)) {
                    logger.logFailure(notification, channel.getClass().getSimpleName(), e);
                    throw new NotificationFailedException(notification, e);
                }
            }
            attempt++;
            sleep(retryPolicy.getDelay(attempt));
        }
    }
}

Step 3: Trace the Flow

mermaid
sequenceDiagram
    participant Client
    participant Service as NotificationService
    participant Channel as EmailChannel
    participant Retry as RetryPolicy
    participant Log as NotificationLogger
    participant SMTP as SmtpClient
 
    Client->>Service: notify(user, notification)
    Service->>Channel: supports(EMAIL)?
    Channel-->>Service: true
    Service->>Channel: send(notification, user)
    Channel->>SMTP: send(email, message)
    SMTP-->>Channel: ❌ ConnectionTimeout
    Channel-->>Service: throws Exception
 
    Service->>Retry: shouldRetry(attempt=0, error)?
    Retry-->>Service: true (delay: 1000ms)
    Note over Service: Wait 1 second
 
    Service->>Channel: send(notification, user) [retry 1]
    Channel->>SMTP: send(email, message)
    SMTP-->>Channel: ✅ success
    Channel-->>Service: true
 
    Service->>Log: logSuccess(notification, "EmailChannel")
Sequence diagram — sending a notification with retry

Why This Design Is Good

PrincipleHow It's Applied
Single ResponsibilityEmailChannel only knows how to send email. RetryPolicy only handles retry logic. NotificationLogger only logs.
Open-ClosedAdding WhatsApp? Create WhatsAppChannel implements NotificationChannel. Zero changes to existing classes.
Dependency InversionNotificationService depends on the NotificationChannel interface, not concrete classes. Swap implementations freely.
CompositionNotificationService is composed of channels, a retry policy, and a logger — all injected, all replaceable.

What You'll Learn in This Series

This series takes you from fundamentals to solving real LLD interview problems. Everything is in Java.

mermaid
graph LR
    C0["Class 0<br/>What is LLD<br/>(you are here)"]
    C1["Class 1<br/>SOLID Principles"]
    C2["Class 2<br/>UML Class Diagrams"]
    C3["Class 3-8<br/>Design Patterns<br/>(Creational, Structural,<br/>Behavioral)"]
    C4["Class 9+<br/>LLD Problems<br/>(Parking Lot, Elevator,<br/>BookMyShow, Chess)"]
 
    C0 --> C1 --> C2 --> C3 --> C4
 
    style C0 fill:#3b82f6,stroke:#2563eb,color:#fff
    style C4 fill:#22c55e,stroke:#16a34a,color:#fff
LLD learning path
1

SOLID Principles (Class 1)

The five rules that form the backbone of every good LLD decision. Each principle explained with a Java "before and after" — the wrong way, why it's wrong, and the fix.

2

UML Class Diagrams (Class 2)

How to read and draw class diagrams. Association, aggregation, composition, inheritance, dependency — each relationship type with Java code mapping. Interviewers expect you to sketch these.

3

Design Patterns (Classes 3–8)

Creational (Singleton, Factory, Builder, Prototype), Structural (Adapter, Decorator, Proxy, Facade, Composite), and Behavioral (Strategy, Observer, Command, State, Template Method, Chain of Responsibility). Each pattern with a real-world Java use case and class diagram.

4

LLD Interview Problems (Classes 9+)

Parking Lot, Elevator System, BookMyShow, Splitwise, Chess, Tic-Tac-Toe, Library Management, Vending Machine — full class-level designs with runnable Java code. The kind of problems you'll face at Amazon, Google, Flipkart, and Razorpay.

How to Approach an LLD Interview

mermaid
graph TD
    subgraph Phase1["Minutes 0-5: Clarify"]
        A1["Ask about scope"]
        A2["Ask about scale"]
        A3["List core use cases"]
    end
 
    subgraph Phase2["Minutes 5-15: Core Design"]
        B1["Identify key entities"]
        B2["Define relationships"]
        B3["Draw class diagram"]
    end
 
    subgraph Phase3["Minutes 15-35: Implementation"]
        C1["Write key interfaces"]
        C2["Implement core classes"]
        C3["Show design patterns used"]
    end
 
    subgraph Phase4["Minutes 35-45: Extend"]
        D1["Walk through a use case"]
        D2["Show how to add a new feature"]
        D3["Discuss trade-offs"]
    end
 
    Phase1 --> Phase2 --> Phase3 --> Phase4
 
    style Phase1 fill:#dbeafe,stroke:#3b82f6
    style Phase2 fill:#dcfce7,stroke:#22c55e
    style Phase3 fill:#fef3c7,stroke:#f59e0b
    style Phase4 fill:#f3e8ff,stroke:#a855f7
LLD interview framework — 45-minute breakdown

What's Next

In the next class, we'll dive deep into the SOLID Principles — the five rules that form the backbone of every good LLD decision. Each principle will have a Java "before and after" example showing exactly what goes wrong when you violate it and how to fix it.

Free, no spam, unsubscribe anytime

Get every deep-dive in your inbox

New case studies, LLD patterns in Java, and HLD architectures on AWS — the full article delivered to you so you never miss a deep-dive.

Full case studies in emailJava + AWS ecosystemOne email per new article