AlgoDaily

Low-Level Design Principles

Read at your own pace. Code execution, quizzes, progress tracking, and the AI tutor are available in the interactive view with JavaScript enabled.

Why LLD Matters

From blueprint to buildable code

In Introduction to System Design and Architecture, you learned that requirements drive design. In High-Level System Design, you turned an idea into a system blueprint: boundaries, data flow, and tradeoffs.

Now we zoom in.

Low-Level Design, or LLD, answers:

  • What classes do we need?
  • What should each class own?
  • Which methods expose behavior?
  • Which details should stay hidden?
  • How do objects collaborate without becoming tangled?

Think of HLD as the city map. LLD is the floor plan, wiring, plumbing, and door placement for each building.

Why LLD Matters

Blueprint To Classes

LLD starts with behavior

A common beginner mistake is to start by listing nouns and making one class per noun. That can work sometimes, but better LLD starts with use cases.

Example requirement:

A customer can add items to a cart, see the total, and place an order.

LLD questions:

Use case: Place order

Cart -------- calculates subtotal
Pricing ----- applies tax/discount rules
Order ------- represents confirmed purchase
Payment ----- charges customer
Receipt ----- formats confirmation

The goal is not to create many classes. The goal is to create small, understandable units of behavior.

A good LLD class usually has:

  • A clear responsibility
  • Hidden internal state
  • Public methods that describe useful actions
  • Few assumptions about unrelated classes

Blueprint To Classes

Single Responsibility

One reason to change

A class should have one primary reason to change.

That does not mean one method. It means the class owns one focused responsibility.

For example, in an order flow:

  • Order stores order lines
  • PriceCalculator computes totals
  • ReceiptPrinter formats output

If tax rules change, we should not edit the receipt formatting class. If receipt wording changes, we should not edit the price calculation class.

Here is a small standalone Java example:

```java
import java.util.*;

public class Main {
  static class OrderLine {
    final String name;
    final int priceCents;
    final int quantity;

    OrderLine(String name, int priceCents, int quantity) {
      this.name = name;
      this.priceCents = priceCents;
      this.quantity = quantity;
    }

    int lineTotal() {
      return priceCents * quantity;
    }
  }

  static class Order {
    private final List<OrderLine> lines = new ArrayList<>();

    void addLine(String name, int priceCents, int quantity) {
      lines.add(new OrderLine(name, priceCents, quantity));
    }

    List<OrderLine> lines() {
      return Collections.unmodifiableList(lines);
    }
  }

  static class PriceCalculator {
    int subtotal(Order order) {
      int sum = 0;
      for (OrderLine line : order.lines()) {
        sum += line.lineTotal();
      }
      return sum;
    }

    int tax(Order order) {
      return Math.round(subtotal(order) * 0.08f);
    }

    int total(Order order) {
      return subtotal(order) + tax(order);
    }
  }

  static class ReceiptPrinter {
    String print(Order order, PriceCalculator calculator) {
      return "Subtotal: " + money(calculator.subtotal(order))
          + "\nTax: " + money(calculator.tax(order))
          + "\nTotal: " + money(calculator.total(order));
    }

    private String money(int cents) {
      return String.format("$%.2f", cents / 100.0);
    }
  }

  public static void main(String[] args) {
    Order order = new Order();
    order.addLine("Keyboard", 5000, 1);
    order.addLine("Mouse", 2500, 2);

    PriceCalculator calculator = new PriceCalculator();
    ReceiptPrinter printer = new ReceiptPrinter();

    System.out.println(printer.print(order, calculator));
  }
}
```

LLD Focus Check

Which option is the best example of a low-level design decision?

Encapsulation Guards Rules

Hide state, expose behavior

Encapsulation means internal data should not be freely changed from the outside.

Instead of exposing fields like balanceCents, expose meaningful actions like:

  • deposit()
  • withdraw()
  • balance()

This lets the class protect its own rules, also called invariants.

Example invariant:

A bank account balance must never become negative.

The class should enforce that rule itself.

```java
public class Main {
  static class BankAccount {
    private int balanceCents;

    BankAccount(int openingBalanceCents) {
      if (openingBalanceCents < 0) {
        throw new IllegalArgumentException("Opening balance cannot be negative");
      }
      this.balanceCents = openingBalanceCents;
    }

    void deposit(int cents) {
      if (cents <= 0) {
        throw new IllegalArgumentException("Deposit must be positive");
      }
      balanceCents += cents;
    }

    void withdraw(int cents) {
      if (cents <= 0) {
        throw new IllegalArgumentException("Withdrawal must be positive");
      }
      if (cents > balanceCents) {
        throw new IllegalStateException("Insufficient funds");
      }
      balanceCents -= cents;
    }

    String balance() {
      return String.format("$%.2f", balanceCents / 100.0);
    }
  }

  public static void main(String[] args) {
    BankAccount account = new BankAccount(10000);
    account.deposit(2500);
    account.withdraw(4000);
    System.out.println("Balance: " + account.balance());

    try {
      account.withdraw(20000);
    } catch (Exception ex) {
      System.out.println("Rejected: " + ex.getMessage());
    }
  }
}
```

Public Fields Safer?

True or false: Exposing mutable fields directly is usually safer than providing methods, because callers can change object state faster.

Depend On Interfaces

Collaborate through contracts

In Java and Spring Boot, you often want a service to depend on what another component can do, not on the exact concrete class.

That is where interfaces help.

Instead of this mental model:

CheckoutService knows every detail of StripeCardGateway

Prefer this:

CheckoutService depends on PaymentGateway behavior
PaymentGateway can be implemented by Card, Wallet, Test Fake, etc.

This keeps LLD flexible without jumping into full microservices yet. We are still designing clean in-process code.

```java
public class Main {
  interface PaymentGateway {
    boolean charge(String userId, int cents);
  }

  static class FakeCardGateway implements PaymentGateway {
    public boolean charge(String userId, int cents) {
      System.out.println("Charging card for user " + userId + ": " + cents + " cents");
      return true;
    }
  }

  static class CheckoutService {
    private final PaymentGateway paymentGateway;

    CheckoutService(PaymentGateway paymentGateway) {
      this.paymentGateway = paymentGateway;
    }

    void checkout(String userId, int totalCents) {
      boolean paid = paymentGateway.charge(userId, totalCents);
      if (paid) {
        System.out.println("Order confirmed");
      } else {
        System.out.println("Payment failed");
      }
    }
  }

  public static void main(String[] args) {
    PaymentGateway gateway = new FakeCardGateway();
    CheckoutService checkoutService = new CheckoutService(gateway);
    checkoutService.checkout("user-42", 7999);
  }
}
```

Cohesion Check

A class whose methods and data all support one focused purpose has high _____.

Design With Scenarios

A practical LLD workflow

When designing a class-level solution, use scenarios to avoid vague diagrams.

Step 1: Pick one use case

Example: Customer checks out cart.

Step 2: Write the object conversation
Controller -> CheckoutService: checkout(userId)
CheckoutService -> Cart: getItems()
CheckoutService -> PriceCalculator: total(cart)
CheckoutService -> PaymentGateway: charge(userId, total)
CheckoutService -> OrderRepository: save(order)
Step 3: Assign responsibilities

Ask:

  • Who owns this data?
  • Who enforces this rule?
  • Who should not know this detail?
  • What would be painful to test?
Step 4: Check for smells

Watch for:

  • Giant classes
  • Methods with too many parameters
  • Classes that know too much about each other
  • Business rules scattered across controllers

LLD is successful when the code feels boring, predictable, and easy to change.

Design With Scenarios

LLD Handoff

What you should now be able to do

You can now take a high-level component from the previous lesson and design its inside structure.

For each important feature, you should be able to define:

  • Classes and their responsibilities
  • Public methods and hidden state
  • Interfaces between collaborators
  • Rules each class protects
  • A simple object interaction flow
A useful LLD checklist
[ ] Does each class have one clear responsibility?
[ ] Is important state private?
[ ] Are business rules enforced near the data they protect?
[ ] Do services depend on interfaces where flexibility matters?
[ ] Can I test this class without starting the whole app?
[ ] Would a new developer understand where to make a change?

Next, we will zoom back out slightly and see how these clean components become independently deployable services in Microservices Architecture.

LLD Handoff

Open interactive view →