Low-Level Design
Design foundations

Introduction to LLD

What is LLD, why do we need it, and how does it differ from System Design? Start with the concept and build a small, testable model.

35 min · includes practiceJava
English lesson availability

The first six LLD lessons are available in English. The other 12 lessons currently have Azerbaijani bodies.

Explain the purpose of LLD, choose responsibility and state boundaries, and justify a small design with behavior tests.

What is Low-Level Design?

Low-Level Design (LLD) determines how code is organized within a feature or module. Which objects exist, what is each responsible for, what data does it hold, and how do they work together? The goal is to turn a requirement into clear responsibilities, methods, and testable behavior.

Answering "What should the system do?" is the starting point. LLD takes the next step: which part will perform that behavior, where will state live, which changes will be allowed, and what will happen when an invalid request arrives?

Low-level does not mean assembly language or processor-level work here. The comparison is with the overall system architecture: we move from the broad view of services to the classes, interfaces, and behavioral rules within a module.

Why do we need LLD?

A feature may work the first time, but scattered rules make it difficult to change. The same check appears in three places, code that changes one field forgets another rule, and an error leaves an object in a partially updated state. LLD reduces these risks through clear boundaries of responsibility and ownership.

These boundaries give a team a shared understanding. A method's name, inputs, and result show another developer how to use it. When not everyone can change the internal data, protecting a rule and testing it in isolation become easier.

  • Correctness: prevent state transitions that are not allowed.
  • Readability: find where a particular rule lives in the code.
  • Testability: check behavior independently of the UI and database.
  • Change: limit which parts a new requirement affects.

The difference between LLD, OOD, and System Design

System Design examines the major parts of a system and how they work together: service boundaries, data storage, traffic, reliability, and distributed-system tradeoffs. LLD models data, behavior, and state transitions within a chosen part.

These are connected concerns. An architectural decision affects a module's contract, and the larger system must account for the module's limits. The discussion differs in scale and level of detail.

Object-Oriented Design (OOD) means designing with objects. In interviews, OOD and LLD often refer to similar formats. LLD is the broader concept; in this course, we mainly study it through Java classes, interfaces, and collaboration between objects.

QuestionSystem Design perspectiveLLD perspective
What do we separate?Services, storage, and messaging componentsClasses, interfaces, and responsibilities
What do we model?Data flow between services and failure boundariesMethod calls and object state transitions
What do we justify?Choices about scale, reliability, and consistencyCorrectness, ownership, dependencies, and ease of change
What does the result look like?An architecture diagram and explanations of decisionsClass contracts, pseudocode, or working code

Two views of the same product

Consider a taxi-booking product. In a System Design discussion, we would show the flow between driver locations, the matching service, trip data, and notifications. Peak traffic, data updates, and behavior during a service outage would be central questions.

In an LLD discussion, we would look inside a trip: which states does Trip hold? Can a trip start before a driver is assigned? Can a completed trip be canceled? If pricing changes, must the trip's transition rules change too?

These questions do not decide the number of classes in advance. We clarify the behavior first, then choose which part owns it.

ResponsibilityPossible modelQuestion it answers
Trip lifecycleTrip and TripStateWhich transition is allowed from the current state?
Pricing rulePricingPolicyCan pricing change independently of trip rules?
Location dataLocationWhich validated value do the coordinates represent?

How will we work through this course?

We will first learn how to present a design, separate responsibilities, and use object-oriented concepts. We will apply design patterns where they address a specific need for change. Naming a pattern does not replace explaining a decision.

Then we will move to concurrent work: updating shared data correctly (correctness), workers waiting for one another and handing off data (coordination), and sharing limited resources (scarcity). We will apply these concepts in practical designs.

The course contains 9 practical designs: Connect Four, Amazon Locker, an elevator, a parking lot, a file system, movie ticket booking, a logging service, a rate limiter, and inventory management. Each example starts with understanding the problem, then moves through requirements, responsibilities, class design, and complete Java code.

  • A basic understanding of Java classes, methods, collections, and exceptions is enough; you do not need to memorize every pattern beforehand.
  • Predict the result before advancing to the next step in a visual example.
  • Before opening the complete code, write down the main classes and 2-3 method contracts.
  • After reading the solution, add a new requirement and explain which part changes.

What is expected in an interview?

An LLD interview usually gives you a bounded problem and asks you to design its code structure. Expectations vary by company, team, and level. Some formats call for pseudocode; others require a working, tested program.

Clarify the time available, the language to use, whether the code will be run, and whether concurrent requests are in scope. Ask about the actual expectations rather than guessing the format from the company's location or size.

You: Is showing the classes and main methods enough, or should I compile and test the code too?

Interviewer: In this format, we want to see the main flow working and tests for boundary cases.

You: Will the model run within one process? Are a database and concurrent requests required?

Interviewer: An in-memory model is enough. We will discuss concurrent requests as a later requirement.

How is a design evaluated?

In a strong answer, decisions connect: a requirement becomes a responsibility, the responsibility becomes a method, and the method leads to a test. You should be able to explain why each class exists and which rule it protects.

A weak start: structure without requirements

Writing ten classes and several patterns as soon as you hear the problem does not clarify its scope. You then have to fit the requirements into that structure.

A stronger start: from behavior to responsibility

First describe one successful flow and one failed flow. Write down the rule that must hold, choose the object that will protect it, and then define the method signatures.

CriterionWhat does a strong answer show?
Understanding the problemThe main flow, scope, and failure cases are clarified before coding.
Class designState ownership and each part's responsibility are clear.
Code qualityNames show intent, input is validated, and internal state is protected.
Ease of changeThere is a suitable boundary for a new requirement, without unnecessary generalization.
CommunicationThe reason for a choice, its alternatives, and its limits are explained clearly.
VerificationTests cover forbidden transitions and state after errors, as well as successful behavior.

A small example: lending a book copy

To make the concepts concrete, let us model one physical copy of a book. Its title and author are catalog information. Our question is different: who currently has this copy, and can another reader borrow it?

The scope is small: a copy is lent to one reader and returned by that reader. Search, fines, user authentication, and reservation queues are excluded. The reader identifier is a nonblank string received from a trusted input boundary.

Invariant: a copy has at most one borrower at a time. If another reader tries to borrow a copy that is already on loan, the borrower must not change. Each step in the scenario below represents one operation.

Introduction to LLD: step by step

Invariant: A copy has at most one borrower.

An available copy

Nobody has borrowed the book yet.

Reader A
No request
Book copy
AVAILABLE
Reader B
No request

Active loans: 0

Behavior first, structure second

A book's title and author do not tell us its lending rule. "The same copy can be held by only one reader at a time" points directly to a model: BookCopy stores the current reader identifier in borrower and checks whether the copy is available in borrow. This is an invariant, a rule that must remain true after every allowed operation.

We do not give client code an unrestricted setter called setBorrower. The borrow and returnBy methods express intent and protect the conditions for a transition. A small API with clear names helps prevent misuse and makes tests easier to write.

The example's contract and class boundary

BookCopy keeps the current borrower in a private field. Callers cannot change it directly; they express intent through borrow(reader) and returnBy(reader). Validation and the state change therefore stay within the same responsibility.

One class is enough for this first example. If we add search across all copies, a separate service can find a copy and forward the request to it. A limit on a reader's total loans needs an owner whose scope extends beyond a single copy.

OperationConditionResult
borrow(reader)Reader is nonblank and the copy is availabletrue; reader becomes the current borrower
borrow(reader)The copy is already on loanfalse; the previous borrower is retained
returnBy(reader)Reader is the current borrowertrue; the copy becomes available
returnBy(reader)The copy is available or reader is someone elsefalse; state does not change
Both methodsReader is null or blankIllegalArgumentException; state does not change

How does the code protect the rule?

borrow validates the input first, then records the borrower if the copy is available. returnBy clears the field only when the current borrower matches. Here, null means there is no active borrower; we do not keep a separate borrowed flag.

The methods use synchronized so calls on the same Java object cannot interleave their check-and-change steps. You do not need every detail of locking yet. The main idea is to keep checking state and changing it together. We will study this in the concurrency module.

This protection applies only within the process that holds that object. It does not coordinate with a separate object on another server. Adding persistent storage and multiple servers requires an appropriate transaction and consistency contract.

Verify the design through behavior

Let A borrow the copy and reject B's attempt to borrow it. Next, let B try to return it: the result should still be false, and A's loan must remain intact. B can borrow the copy only after A returns it. This sequence checks both the result and the state being protected.

Test null and blank identifiers separately. After an exception, verify that the previous borrower has not changed. Checking only that an exception was thrown may miss a partially modified state.

Building every future requirement now

If fines are not required, adding a PaymentGateway, an event bus, and a large inheritance tree obscures the main rule.

Revisiting the boundary when a new requirement arrives

If history is needed, add separate loan records. If the requirement becomes "A reader may borrow at most 3 books," choose a rule owner that covers all copies. Extend the model without losing the current copy's rule.

Prepare for the next lesson

Try explaining LLD in one sentence: we turn the behavior of a bounded problem into clear responsibilities, methods, and protected state. We judge a design by the correctness of its rules and how well it accommodates change, rather than by the number of boxes or patterns.

In the next lesson, we will follow a sequence that fits an interview: clarify the problem, choose the main flow, build the model, implement the critical behavior, and verify the design. For now, run the code below and work through the reader-limit exercise yourself.

  • I can explain the difference between LLD and System Design using the same product.
  • I can write an invariant and identify the object that protects it.
  • I can distinguish successful, rejected, and invalid-input results from a method.
  • I can explain which boundary changes when a new requirement arrives.

Model and responsibilities

Class boundaries and the state they hold
Class / typeResponsibilityState / fields
BookCopyProtects the active borrower and allowed transitions of one physical copy.private String borrower

Complete Java code

Save the files in the same directory: javac BookCopy.java Demo.java, then java Demo. The results will be true, false, false, true, true in that order. The model runs in memory; authentication and persistent storage are not included.

Java
public final class BookCopy {
    private String borrower;

    public synchronized boolean borrow(String reader) {
        validateReader(reader);
        if (borrower != null) return false;
        borrower = reader;
        return true;
    }

    public synchronized boolean returnBy(String reader) {
        validateReader(reader);
        if (!reader.equals(borrower)) return false;
        borrower = null;
        return true;
    }

    private static void validateReader(String reader) {
        if (reader == null || reader.isBlank()) {
            throw new IllegalArgumentException("Oxucu identifikatoru boş ola bilməz");
        }
    }
}

BookCopy c = new BookCopy(); check(!c.returnBy("A")); check(c.borrow("A")); check(!c.borrow("A")); check(!c.borrow("B")); check(!c.returnBy("B")); for (String reader : new String[]{null, "", " "}) { try { c.borrow(reader); throw new AssertionError(); } catch (IllegalArgumentException expected) {} try { c.returnBy(reader); throw new AssertionError(); } catch (IllegalArgumentException expected) {} check(!c.borrow("B")); } check(c.returnBy("A")); check(!c.returnBy("A")); check(c.borrow("B")); check(c.returnBy("B")); Demo.main(new String[0]);

Continue with the course

The first six LLD lessons are available in English. The other 12 lessons currently have Azerbaijani bodies.

Create an account
Introduction to LLD - free sample lesson | Proxy Academy