Low-level designCore11 min
How to approach an LLD problem
Agree on what to build, find the rules that will change and keep them apart, name the things, give each rule an owner, give a pattern only to the parts that vary, draw the classes, walk the main use cases, and only then write the core code. Each step catches mistakes the next one would bake in.
Design a library's holds
Members place holds; a returned copy goes to the next member by the title's rule; at most two holds each.
1 · The problem
"Design the holds system for a library"
That's the whole brief, which is typical: a low-level design problem arrives as one vague sentence. It could mean a hundred different programs. The way through is the same eight steps every time, in order:
Step | You produce | Roughly, in 45 minutes |
|---|---|---|
Requirements | What's in scope, what's out, the rules | 5 minutes |
What varies | The rules that come in several versions, kept apart from the rest | 2 to 3 minutes |
Entities | The things in the problem | 3 to 5 minutes |
Responsibilities | What each thing knows and does | 5 minutes |
Patterns | A pattern for each part that varies, and nothing else | 2 to 3 minutes |
Classes and relationships | A class diagram | 5 to 10 minutes |
Interactions | The calls for each main use case | 5 minutes |
Code | The core classes, run on an example | 10 to 15 minutes, and what's left for follow-ups |
The minutes are a guide for an interview; at work the same steps happen in a design document or a whiteboard session. What matters is the order: every step uses the answer to the one before it.
2 · Requirements
Agree on what to build before designing it
Ask until the brief becomes rules you could test. A few questions settle most of it:
- Who uses it? Members place holds; staff return copies.
- What happens when a copy comes back? Usually it goes to the member who has waited longest, and is kept for them. Course books on the reserve shelf go to staff first, even if a student asked earlier.
- Are there limits? A member may have at most two holds at once.
- Can a hold be canceled? Yes, and if a copy was waiting for that member, it passes to the next in line.
In scope | Out of scope (said out loud) |
|---|---|
Place a hold on a title | Searching the catalog |
Return a copy, which goes to the next hold by the title's rule | Due dates and late fees |
Cancel a hold | How long a ready copy is kept before it passes on |
Pick up a ready copy | Notifications by email or text |
Saying what's out of scope is part of the answer. It keeps the design small, and it shows you saw those features and chose to leave them for later.
3 · What varies
Find the rules that change, and keep them apart
Before naming a single class, go back through the rules and ask of each one: does it come in more than one version, or is it already changing? Those rules get kept apart from the code that stays the same, so that changing them later never means editing everything else. This step decides where the design splits into parts that change and parts that don't.
Rule | Varies? | Why |
|---|---|---|
A returned copy goes to the next member | Yes | Two versions today: first in line for most titles, staff first for course books |
A member may have at most two holds | No | One number for everyone; if it changes, it's a constant that changes |
A canceled ready hold passes its copy on | No | The same for every title |
A hold is waiting, ready, collected or canceled | No | A closed set, with the same rules for every hold |
This is the principle called encapsulate what varies. Apply it to what has varied, not to what might: a rule with a second version today, or a change the library has already asked for. A rule that might change one day gets no special treatment; it can be separated when that day comes.
4 · Entities
Find the nouns, then decide which are classes
Read the rules and pick out the nouns: member, title, copy, hold, line, library. Each one becomes a class, a field, or nothing. A noun earns a class when it has state that changes or rules of its own.
Noun | Decision | Why |
|---|---|---|
Member | Class | Has a name and a number of active holds, and the limit rule |
Staff member | A flag on | Staff follow the same rules as everyone else, except one; that one lives with the rule that varies |
Title | Class | Has its copies and its line of holds |
Hold | Class | Changes over time: waiting, ready, collected or canceled |
Hold's status | Enum | A closed set of four values |
Copy | A count on | No rule tells one copy of a title from another, so a number is enough |
Line | A queue inside | It's the order of a title's holds, not a thing with its own rules |
Library | Nothing, for now | No rule here belongs to the library as a whole |
The verbs become methods: place a hold, return a copy, cancel, pick up. Who gets each one is the next step.
5 · Responsibilities
Give every rule an owner
For each rule, ask which object already has the data it needs, and give the rule to that object. Asking an object to do something with its own data, rather than pulling the data out and deciding elsewhere, keeps each rule in one place.
Class | Knows | Does |
|---|---|---|
| Its name, whether it's staff, how many holds it has | Answers |
| Its member and its status | Becomes ready; ends as collected or canceled, telling its member |
| Its free copies, its line of holds, its line policy | Places holds, gives a returned copy to the hold its policy picks, handles cancels |
| Nothing of its own | Picks which waiting hold gets a returned copy |
Notice what's missing: a LibraryManager that knows every member, title and hold and runs all the rules. That class is easy to start and hard to live with, because every rule ends up in it. Here each rule sits with the data it reads.
6 · Patterns
A pattern for each part that varies, and nowhere else
Now take each rule that varies and give it an interface. The code that uses the rule is written against the interface, and each version of the rule is a class behind it. That's what a design pattern is for: a known shape for a known kind of change.
What varies | Pattern | How it looks here |
|---|---|---|
Who gets a returned copy | Strategy | A |
Title calls next() on its policy and never asks which kind it has. That's programming to an interface: a third rule, say members who've never held a copy go first, is one new class, and Title doesn't change.
Every other rule has one version, so it gets no pattern. A factory for holds or an observer for members here would be code with nothing varying behind it. Picking patterns before finding what varies is one of the most common ways an LLD answer goes wrong.
7 · Classes
Draw the classes and how they relate
Now write the classes with their fields only, plus the one interface the last step decided, and draw them. This skeleton is cheap to change, and the diagram shows whether the relationships make sense before any logic depends on them.
enum HoldStatus { WAITING, READY, COLLECTED, CANCELED }
class Member {
private final String name;
private final boolean staff;
private int activeHolds;
Member(String name, boolean staff) {
this.name = name;
this.staff = staff;
}
String name() {
return name;
}
boolean isStaff() {
return staff;
}
}
class Hold {
private final Member member;
private HoldStatus status = HoldStatus.WAITING;
Hold(Member member) {
this.member = member;
}
Member member() {
return member;
}
}
interface LinePolicy {
Hold next(List<Hold> waiting);
}
class FirstInLine implements LinePolicy {
public Hold next(List<Hold> waiting) {
return waiting.get(0);
}
}
class StaffFirst implements LinePolicy {
public Hold next(List<Hold> waiting) {
for (Hold h : waiting) {
if (h.member().isStaff()) return h;
}
return waiting.get(0);
}
}
class Title {
private final String name;
private int freeCopies;
private final List<Hold> waiting = new ArrayList<>();
private final LinePolicy policy;
Title(String name, int copies, LinePolicy policy) {
this.name = name;
this.freeCopies = copies;
this.policy = policy;
}
String name() {
return name;
}
int freeCopies() {
return freeCopies;
}
}Drawn from the skeleton: a Title keeps a line of many holds and has one LinePolicy, which FirstInLine and StaffFirst implement; each Hold belongs to one Member and has a HoldStatus.
Read the arrows as sentences, and check each one against the rules: a title has many holds in line and one rule for who's next; a hold is for exactly one member; a member doesn't point to its holds, because no rule needs to list them, only count them. The next page, UML class diagrams, explains what each kind of line means.
8 · Interactions
Walk each use case through the objects
Take the trickiest use case and trace the calls before writing them. Ana holds the only copy of Dune, ready for her, and Ben is next in line. Ana cancels:
One cancel, as calls, starting when the desk calls cancel on Dune. The title notices the hold was ready and passes its copy on, by the same path a returned copy takes: its line policy picks who's next.
The trace finds two things the skeleton needs: a canceled hold must tell its member, so the member's count goes down, and a canceled ready hold must pass its copy on. Finding those here costs a line on a whiteboard; finding them in code costs a rewrite.
9 · Code
Write the core, then run an example
Add the behavior the steps above decided. Each method is short because each rule already has a home:
enum HoldStatus { WAITING, READY, COLLECTED, CANCELED }
class Member {
static final int MAX_HOLDS = 2;
private final String name;
private final boolean staff;
private int activeHolds;
Member(String name, boolean staff) {
this.name = name;
this.staff = staff;
}
String name() {
return name;
}
boolean isStaff() {
return staff;
}
boolean canHold() {
return activeHolds < MAX_HOLDS;
}
void holdStarted() {
activeHolds++;
}
void holdEnded() {
activeHolds--;
}
}
class Hold {
private final Member member;
private HoldStatus status = HoldStatus.WAITING;
Hold(Member member) {
this.member = member;
}
Member member() {
return member;
}
HoldStatus status() {
return status;
}
void markReady() {
status = HoldStatus.READY;
}
void collect() {
end(HoldStatus.COLLECTED);
}
void cancel() {
end(HoldStatus.CANCELED);
}
private void end(HoldStatus last) {
if (status == HoldStatus.COLLECTED || status == HoldStatus.CANCELED) {
throw new IllegalStateException("This hold has already ended");
}
status = last;
member.holdEnded();
}
}
interface LinePolicy {
Hold next(List<Hold> waiting);
}
class FirstInLine implements LinePolicy {
public Hold next(List<Hold> waiting) {
return waiting.get(0);
}
}
class StaffFirst implements LinePolicy {
public Hold next(List<Hold> waiting) {
for (Hold h : waiting) {
if (h.member().isStaff()) return h;
}
return waiting.get(0);
}
}
class Title {
private final String name;
private int freeCopies;
private final List<Hold> waiting = new ArrayList<>();
private final LinePolicy policy;
Title(String name, int copies, LinePolicy policy) {
this.name = name;
this.freeCopies = copies;
this.policy = policy;
}
String name() {
return name;
}
// A free copy goes to the new hold at once; otherwise it joins the end of the line.
Optional<Hold> placeHold(Member m) {
if (!m.canHold()) return Optional.empty();
Hold h = new Hold(m);
m.holdStarted();
if (freeCopies > 0) {
freeCopies--;
h.markReady();
} else {
waiting.add(h);
}
return Optional.of(h);
}
// A returned copy goes to the hold the policy picks, or back on the shelf.
Optional<Hold> returnCopy() {
if (waiting.isEmpty()) {
freeCopies++;
return Optional.empty();
}
Hold next = policy.next(waiting);
waiting.remove(next);
next.markReady();
return Optional.of(next);
}
// A canceled ready hold passes its copy on.
Optional<Hold> cancel(Hold h) {
boolean wasReady = h.status() == HoldStatus.READY;
waiting.remove(h);
h.cancel();
return wasReady ? returnCopy() : Optional.empty();
}
}The same classes with their behavior. Title.placeHold() takes a Member (the dashed arrow), and Title uses its LinePolicy without knowing which one it is.
Then run the cancel traced above, the limit, and a course book:
Ana holds Dune: ready Ben holds Dune: waiting Ben holds Emma: waiting Ben holds Ulysses: refused, 2 holds already Ana cancels Dune: ready for Ben Emma returned: ready for Ben Ben picks up Dune Ben holds Ulysses: waiting Cy holds Chemistry: waiting Dee holds Chemistry: waiting Chemistry returned: ready for Dee
Ben's third hold is refused at the limit of two; once he picks up Dune, it's accepted. The returned Chemistry copy goes to Dee, who is staff, although Cy asked first.
10 · Common mistakes
Where answers usually go wrong
Mistake | Why it hurts | Instead |
|---|---|---|
Coding first | The code commits to a scope nobody agreed on | Spend the first minutes on requirements |
Skipping what varies | The changing rule becomes an | Right after the requirements, find the rules with more than one version |
A class for every noun |
| A noun becomes a class when it has changing state or rules |
One manager class | Every rule lands in it, and it changes for every reason | Give each rule to the object that has its data |
Getters and setters everywhere | Callers pull data out and decide, so the rules scatter | Ask objects to act: |
Patterns first | Factories and observers with nothing varying behind them | Find what varies first, and give a pattern only to those parts |
With time left, follow-ups test the design: a ready copy is kept for three days, then passes on, or a third rule for who's next. The first is a date on Hold and one more method on Title; the second is one new LinePolicy class. Being able to say where a change lands is the point of the steps.
11 · In practice
Try the steps on a full problem
The practice problems follow the same steps. Each starts with a brief and the rules, and the checks read your classes as you write them: missing relationships, rules in the wrong class, and one class doing several jobs.
Design the parking lot, starting from its requirements.
Check yourself
5 questions
Takeaways
Remember this
- Eight steps, in order: requirements, what varies, entities, responsibilities, patterns, classes, interactions, code.
- Right after the requirements, find the rules that come in several versions and keep them apart from the code that stays the same.
- A noun becomes a class when it has changing state or rules of its own; otherwise it's a field or nothing.
- Give each rule to the object that already has the data it needs, rather than to a manager class.
- A pattern answers a part that varies; with nothing varying behind it, it's only extra code.