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.

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

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:
-
Orderstores order lines -
PriceCalculatorcomputes totals -
ReceiptPrinterformats 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.

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.
