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.
| Question | System Design perspective | LLD perspective |
|---|---|---|
| What do we separate? | Services, storage, and messaging components | Classes, interfaces, and responsibilities |
| What do we model? | Data flow between services and failure boundaries | Method calls and object state transitions |
| What do we justify? | Choices about scale, reliability, and consistency | Correctness, ownership, dependencies, and ease of change |
| What does the result look like? | An architecture diagram and explanations of decisions | Class 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.
| Responsibility | Possible model | Question it answers |
|---|---|---|
| Trip lifecycle | Trip and TripState | Which transition is allowed from the current state? |
| Pricing rule | PricingPolicy | Can pricing change independently of trip rules? |
| Location data | Location | Which 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.
| Criterion | What does a strong answer show? |
|---|---|
| Understanding the problem | The main flow, scope, and failure cases are clarified before coding. |
| Class design | State ownership and each part's responsibility are clear. |
| Code quality | Names show intent, input is validated, and internal state is protected. |
| Ease of change | There is a suitable boundary for a new requirement, without unnecessary generalization. |
| Communication | The reason for a choice, its alternatives, and its limits are explained clearly. |
| Verification | Tests 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.
| Operation | Condition | Result |
|---|---|---|
| borrow(reader) | Reader is nonblank and the copy is available | true; reader becomes the current borrower |
| borrow(reader) | The copy is already on loan | false; the previous borrower is retained |
| returnBy(reader) | Reader is the current borrower | true; the copy becomes available |
| returnBy(reader) | The copy is available or reader is someone else | false; state does not change |
| Both methods | Reader is null or blank | IllegalArgumentException; 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.