Lesson 1 of 31
What Is Low-Level Design?
Low-level design (LLD) is the design of the code inside one program or one service: which types exist, what data each owns, which operations they expose, how they depend on each other, and how they behave when requirements change. It is sometimes called object-oriented design (OOD) because the classic exercises are phrased in terms of classes and objects. The output is a set of types and methods, usually with a working core of code.
System design works one level up. It decides which services, databases, queues and caches exist and how they communicate. Low-level design decides what happens inside one of those boxes.
| System design | Low-level design | |
|---|---|---|
| Unit of design | services, databases, queues | types, interfaces, packages |
| Main question | will it scale and stay available? | will it stay correct and easy to change? |
| Typical artifact | boxes and arrows, capacity numbers | class diagram, interfaces, working code |
| Failure it prevents | overload, data loss, outages | tangled code, wrong states, painful changes |
| Example prompt | "Design a URL shortener" | "Design a parking lot" |
Some problems exist at both levels. A rate limiter is a distributed service in the system design version and a small set of types (a RateLimiter interface, a token bucket, a clock) in the low-level one. Both are useful, and they answer different questions.
What a low-level design interview evaluates
An interviewer gives a short, vague prompt such as "design a vending machine" and watches how the candidate turns it into code. The typical criteria are:
- Requirements. Does the candidate ask what the system must do, and what it does not do, before drawing anything?
- Modeling. Are the nouns in the problem turned into sensible types with clear responsibilities?
- Encapsulation. Can a caller put the system into an invalid state? Good designs make that hard.
- Extensibility. When the interviewer adds a requirement ("now support monthly passes"), does the design absorb it with a small change, or does it need a rewrite?
- Patterns used for a reason. Strategy, state, observer and similar patterns should appear because the problem needs them, not because they are on a list.
- Working code. A design that cannot be turned into code in the time available is incomplete. Typing the core flow and running it is the strongest evidence.
- Trade-offs. Can the candidate explain what was chosen, what was rejected, and why?
This course trains exactly those skills, using Java 21 for every example.
What Java provides for the design
Older Java material leans on deep class inheritance, mutable setters and null. Java 21 has types that make the same designs smaller and safer. This course uses them in every example.
| Older style | Java 21 style |
|---|---|
| A base class with subclasses | an interface, or a sealed interface when the set of variants is closed |
A class with fields, getters, equals and hashCode written by hand |
a record |
A null field for "not set" |
Optional<T> as a return type, or a separate type for the state that has the value |
| A boolean plus a string for a state | a sealed interface whose record variants carry their own data |
An if chain over a type tag |
a switch expression with pattern matching, checked for exhaustiveness by the compiler |
| A mutable global shared by everything | an object passed to the constructor (dependency injection) |
| Setters that let any caller change any field | a constructor or factory method that validates, and methods named for the business operation |
The consequence is that a Java design has few mutable classes and more small types. Instead of a Vehicle base class with Car and Truck subclasses, there can be a VehicleKind enum. Instead of a Ticket object with a nullable exitTime, there can be two states of a ticket, and only the second has an exit time. The compiler checks that every state is handled, and a switch over a sealed type stops compiling when a new variant is added and one switch is missed.
Here is a small example. A traditional design models a payment with a status string and nullable fields. The Java version makes the invalid combinations unrepresentable.
public class Main {
sealed interface Payment {
record Pending(long amountCents) implements Payment {}
record Paid(long amountCents, String receipt) implements Payment {}
record Refunded(long amountCents, String reason) implements Payment {}
}
static Payment pay(Payment payment, String receipt) {
return switch (payment) {
case Payment.Pending p -> new Payment.Paid(p.amountCents(), receipt);
case Payment.Paid p -> throw new IllegalStateException("cannot pay a payment that is " + p);
case Payment.Refunded r -> throw new IllegalStateException("cannot pay a payment that is " + r);
};
}
static Payment refund(Payment payment, String reason) {
return switch (payment) {
case Payment.Paid p -> new Payment.Refunded(p.amountCents(), reason);
case Payment.Pending p -> throw new IllegalStateException("cannot refund a payment that is " + p);
case Payment.Refunded r -> throw new IllegalStateException("cannot refund a payment that is " + r);
};
}
public static void main(String[] args) {
Payment payment = new Payment.Pending(2_500);
Payment paid = pay(payment, "R-1001");
System.out.println(paid);
Payment refunded = refund(paid, "damaged item");
System.out.println(refunded);
try {
pay(refunded, "R-1002");
} catch (IllegalStateException e) {
System.out.println(e.getMessage());
}
}
}Paid[amountCents=2500, receipt=R-1001]
Refunded[amountCents=2500, reason=damaged item]
cannot pay a payment that is Refunded[amountCents=2500, reason=damaged item]A refunded payment has no receipt to read by mistake, because the field only exists on Paid. Each transition returns a new value and the records are immutable, so no code can change a Pending payment in place after it has been paid. The switch has no default branch: if a fourth state is added to the sealed interface, this code stops compiling until each switch handles it.
The layers of a design
A useful way to organize an answer is to separate three layers and to design them in this order.
- Domain types. The nouns of the problem and the rules that always hold:
ParkingSpot,Ticket,Money. They contain data and the operations that keep that data valid. They know nothing about I/O, threads or storage. - Policies and behavior. The parts that vary: how the fee is calculated, which elevator is chosen, how the next spot is picked. In Java these are interfaces, lambdas or enums, so they can be replaced without touching the domain types.
- Orchestration. A service type that receives requests, uses the domain types and policies, and returns results:
ParkingLot,ElevatorController,Library. This is the public API of the design.
Keeping the layers apart is what makes a change small. Adding a new pricing rule touches only the policy layer. Adding a new vehicle type touches the domain types and, at most, a switch in the policy. The orchestration code stays the same.
How this course is organized
- Foundations. The interview framework and the mapping from object-oriented concepts to Java types.
- Design principles. SOLID, encapsulation, state machines, error modeling, object graphs and references, and designing for tests.
- Concurrent components. Thread-safe components, event buses and blocking queues, since many problems become harder when two callers act at once.
- Data-structure problems. LRU cache, key-value store with expiry, rate limiter, logger, and scheduler.
- Domain problems. Parking lot, elevator, vending machine, ATM, library, ticket booking, expense sharing, file system, and more.
- Cheat sheets. A checklist to run in an interview and a study plan.
Every example is a complete Java 21 program that is compiled and run, and the printed output is checked by the site's example checker. Only the standard library is used. Checks in examples use plain assert statements, which the checker enables.
The course assumes Java basics: classes, interfaces, generics, collections, and exceptions. For the pattern vocabulary, the low-level design articles apply patterns to complete problems and do not repeat reference material.
Summary
- Low-level design decides the types, operations and dependencies inside one program. System design decides the services around it.
- Interviews grade requirements gathering, modeling, encapsulation, extensibility, justified pattern use, working code and trade-offs.
- Java 21 favors records for values, sealed interfaces for closed sets of variants, interfaces for open sets, and exceptions or sealed result types for failures.
- Design in layers: domain types, then varying policies, then an orchestrating service.
- Make invalid states unrepresentable. It removes whole classes of bugs before any test runs.
