All notes
System Design Low Level Design

Introduction to Low-Level Design (LLD)

6 min read Engineering notes

Low-Level Design (LLD), often referred to as Object-Oriented Design (OOD) or Detailed Component Design, is the phase in software engineering where abstract architectural requirements are translated into detailed class structures, interfaces, relationships, design patterns, and executable code module blueprints.

While High-Level Design (HLD) focuses on system-level components (load balancers, databases, caching layers, microservices, and network protocols), Low-Level Design focuses on code modularity, maintainability, extensibility, testability, and clean object-oriented architecture.


High-Level Design vs. Low-Level Design

AspectHigh-Level Design (HLD)Low-Level Design (LLD)
FocusMacro architecture & system topologyMicro class structure & code modules
Key QuestionsHow do services communicate? How does data flow? How do we scale horizontally?How are responsibilities separated? What design patterns fit best? How do classes interact?
ArtifactsArchitecture diagrams, data flow diagrams, API specs, database schemasClass diagrams (UML), sequence diagrams, interface contracts, design pattern blueprints
ConceptsCaching, Sharding, Replication, Load Balancing, Message QueuesSOLID principles, Design Patterns, Composition over Inheritance, Encapsulation, Polymorphism

Core Pillars of Low-Level Design

1. Object-Oriented Analysis & Design (OOAD)

Object-Oriented Design anchors on four foundational paradigms:

  • Encapsulation: Hiding internal state and exposing functionality strictly through clean public methods.
  • Abstraction: Hiding complex implementation details behind interfaces or abstract classes.
  • Inheritance: Enabling code reuse through hierarchical relationships (used judiciously).
  • Polymorphism: Allowing uniform interaction with heterogeneous object types via common interfaces.

2. Composition Over Inheritance

Inheritance introduces tight coupling between superclasses and subclasses. LLD strongly favors composition (has-a relationships) over inheritance (is-a relationships) to make system components dynamically configurable at runtime.

3. SOLID Design Principles

The SOLID acronym defines 5 guidelines for scalable object-oriented software:

  • S - Single Responsibility Principle (SRP): A class should have one, and only one, reason to change.
  • O - Open/Closed Principle (OCP): Software entities should be open for extension, but closed for modification.
  • L - Liskov Substitution Principle (LSP): Subtypes must be substitutable for their base types without altering program correctness.
  • I - Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they do not use.
  • D - Dependency Inversion Principle (DIP): High-level modules should depend on abstractions, not concrete implementations.

4. Design Patterns

Design patterns are reusable solutions to common recurring design problems:

  • Creational: Singleton, Factory Method, Abstract Factory, Builder, Prototype.
  • Structural: Adapter, Decorator, Facade, Composite, Proxy.
  • Behavioral: Strategy, Observer, Command, State, Chain of Responsibility.

Step-by-Step LLD Methodology

When approaching an LLD problem in interviews or production, follow this structured 6-step framework:

[1. Clarify Requirements] ──> [2. Identify Core Entities] ──> [3. Define Relationships & Classes]
                                                                        │
[6. Code Implementation]  <──  [5. Apply Design Patterns]  <──  [4. Define Interfaces & Enums]
  1. Requirements & Scope: Define functional requirements, edge cases, and explicit constraints.
  2. Identify Core Entities: Extract nouns from the problem statement (e.g., User, Ticket, ParkingSpot, Payment).
  3. Class Relationships: Determine relationships between entities (1:1, 1:N, N:M) and composition vs inheritance.
  4. Interfaces & Enums: Define system states (SpotType, PaymentStatus) and abstract contracts (PaymentProcessor, PricingStrategy).
  5. Apply Design Patterns: Integrate appropriate patterns (e.g., Factory pattern for object creation, Strategy pattern for dynamic pricing algorithms).
  6. Code Implementation: Write clean, modular, thread-safe, and self-documenting code.

Concrete Example: Parking Lot LLD

To see LLD in action, let's look at a classic Low-Level Design problem: Parking Lot System.

Requirements

  1. Support multiple parking spot types (Compact, Large, Handicapped).
  2. Support multiple vehicle types (Car, Bike, Truck).
  3. Dynamic pricing strategies (Hourly, Flat Rate).
  4. Issue a ticket on entry and process payment on exit.

Class Blueprint & Code Implementation (Java)

import java.time.LocalDateTime;
import java.util.*;

// --- Enums ---
enum VehicleType { BIKE, CAR, TRUCK }
enum ParkingSpotType { COMPACT, LARGE, HANDICAPPED }

// --- Vehicle Entity ---
class Vehicle {
    private final String licensePlate;
    private final VehicleType vehicleType;

    public Vehicle(String licensePlate, VehicleType vehicleType) {
        this.licensePlate = licensePlate;
        this.vehicleType = vehicleType;
    }

    public String getLicensePlate() { return licensePlate; }
    public VehicleType getVehicleType() { return vehicleType; }
}

// --- Parking Spot Entity ---
class ParkingSpot {
    private final String spotId;
    private final ParkingSpotType spotType;
    private boolean isOccupied;
    private Vehicle parkedVehicle;

    public ParkingSpot(String spotId, ParkingSpotType spotType) {
        this.spotId = spotId;
        this.spotType = spotType;
        this.isOccupied = false;
    }

    public boolean assignVehicle(Vehicle vehicle) {
        if (isOccupied) return false;
        this.parkedVehicle = vehicle;
        this.isOccupied = true;
        return true;
    }

    public void removeVehicle() {
        this.parkedVehicle = null;
        this.isOccupied = false;
    }

    public String getSpotId() { return spotId; }
    public boolean isOccupied() { return isOccupied; }
}

// --- Strategy Pattern: Pricing Strategy ---
interface PricingStrategy {
    double calculateFee(double hours);
}

class HourlyPricingStrategy implements PricingStrategy {
    private final double ratePerHour;

    public HourlyPricingStrategy(double ratePerHour) {
        this.ratePerHour = ratePerHour;
    }

    @Override
    public double calculateFee(double hours) {
        return Math.max(1.0, hours) * ratePerHour;
    }
}

// --- Ticket Model ---
class Ticket {
    private final String ticketId;
    private final Vehicle vehicle;
    private final ParkingSpot spot;
    private final LocalDateTime entryTime;

    public Ticket(Vehicle vehicle, ParkingSpot spot) {
        this.ticketId = UUID.randomUUID().toString().substring(0, 8);
        this.vehicle = vehicle;
        this.spot = spot;
        this.entryTime = LocalDateTime.now();
    }

    public String getTicketId() { return ticketId; }
    public ParkingSpot getSpot() { return spot; }
}

// --- Main Parking Lot Manager ---
class ParkingLot {
    private final String name;
    private final List<ParkingSpot> spots = new ArrayList<>();
    private final Map<String, Ticket> activeTickets = new HashMap<>();
    private final PricingStrategy pricingStrategy;

    public ParkingLot(String name, PricingStrategy pricingStrategy) {
        this.name = name;
        this.pricingStrategy = pricingStrategy;
    }

    public void addSpot(ParkingSpot spot) {
        spots.add(spot);
    }

    public Ticket parkVehicle(Vehicle vehicle) {
        for (ParkingSpot spot : spots) {
            if (!spot.isOccupied()) {
                spot.assignVehicle(vehicle);
                Ticket ticket = new Ticket(vehicle, spot);
                activeTickets.put(ticket.getTicketId(), ticket);
                return ticket;
            }
        }
        System.out.println("Parking Lot Full!");
        return null;
    }

    public double checkout(String ticketId, double hoursStayed) {
        Ticket ticket = activeTickets.remove(ticketId);
        if (ticket == null) {
            throw new IllegalArgumentException("Invalid Ticket ID");
        }
        ticket.getSpot().removeVehicle();
        return pricingStrategy.calculateFee(hoursStayed);
    }
}

// --- Driver Demonstration ---
public class Main {
    public static void main(String[] args) {
        ParkingLot lot = new ParkingLot("Central Tech Park Lot", new HourlyPricingStrategy(15.0));
        lot.addSpot(new ParkingSpot("A-101", ParkingSpotType.COMPACT));
        lot.addSpot(new ParkingSpot("A-102", ParkingSpotType.LARGE));

        Vehicle car = new Vehicle("KA-01-HH-1234", VehicleType.CAR);
        Ticket ticket = lot.parkVehicle(car);
        System.out.println("Vehicle Parked! Ticket ID: " + ticket.getTicketId() + ", Spot: " + ticket.getSpot().getSpotId());

        double fee = lot.checkout(ticket.getTicketId(), 3.0);
        System.out.printf("Checkout Complete! Total Fee: $%.2f%n", fee);
    }
}

Best Practices & Key Takeaways

  1. Avoid God Classes: Break large managers down into focused entities adhering to the Single Responsibility Principle.
  2. Program to Interfaces: Rely on abstract interfaces (PricingStrategy, PaymentProcessor) rather than concrete implementations so algorithms can be swapped dynamically.
  3. Handle Concurrency: In production environments, protect critical state modifications (e.g., spot reservation) with locks or thread-safe synchronization blocks.
  4. Keep it Simple (KISS): Do not over-engineer with excessive patterns unless the requirements explicitly justify them.