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.

Watch it evolve1 / 10
The problem

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.

80
Start
Stage 1
Final

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 Member

Has a name and a number of active holds, and the limit rule

Staff member

A flag on Member

Staff follow the same rules as everyone else, except one; that one lives with the rule that varies

Title

Class Title

Has its copies and its line of holds

Hold

Class Hold

Changes over time: waiting, ready, collected or canceled

Hold's status

Enum HoldStatus

A closed set of four values

Copy

A count on Title

No rule tells one copy of a title from another, so a number is enough

Line

A queue inside Title

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

Member

Its name, whether it's staff, how many holds it has

Answers canHold() against the limit of two

Hold

Its member and its status

Becomes ready; ends as collected or canceled, telling its member

Title

Its free copies, its line of holds, its line policy

Places holds, gives a returned copy to the hold its policy picks, handles cancels

LinePolicy

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 LinePolicy interface with next(); FirstInLine and StaffFirst implement it, and each Title is given one

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.

The skeleton: state, and the one interfaceJava
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;
    }
}
*«enum»HoldStatusWAITINGREADYCOLLECTEDCANCELEDMember− name: String− staff: boolean− activeHolds: int~ name(): String~ isStaff(): booleanHold− member: Member− status: HoldStatus~ member(): Member«interface»LinePolicy+ next(List<Hold>): HoldFirstInLine+ next(List<Hold>): HoldStaffFirst+ next(List<Hold>): HoldTitle− name: String− freeCopies: int− waiting: List<Hold>− policy: LinePolicy~ name(): String~ freeCopies(): int

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:

The designJava
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();
    }
}
*«enum»HoldStatusWAITINGREADYCOLLECTEDCANCELEDMember~ MAX_HOLDS: int− name: String− staff: boolean− activeHolds: int~ name(): String~ isStaff(): boolean~ canHold(): boolean~ holdStarted()~ holdEnded()Hold− member: Member− status: HoldStatus~ member(): Member~ status(): HoldStatus~ markReady()~ collect()~ cancel()− end(HoldStatus)«interface»LinePolicy+ next(List<Hold>): HoldFirstInLine+ next(List<Hold>): HoldStaffFirst+ next(List<Hold>): HoldTitle− name: String− freeCopies: int− waiting: List<Hold>− policy: LinePolicy~ name(): String~ placeHold(Member): Optional<Hold>~ returnCopy(): Optional<Hold>~ cancel(Hold): Optional<Hold>

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 if inside Title, and every new rule edits it

Right after the requirements, find the rules with more than one version

A class for every noun

Copy and Line classes with no rules add code and nothing else

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: canHold(), not getActiveHolds() < 2

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

1. The brief says "design a library holds system". What do you do first?
2. Course books go to staff first; every other title goes to the first in line. Where does that rule go?
3. "Copy" is a noun in the rules. Why isn't it a class here?
4. Which object should answer whether a member may place another hold?
5. Which part of the holds design should get a design pattern?

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.