The LLD Interview Playbook
A repeatable 45-minute structure, what interviewers actually score, and the mistakes that cost points.
- 45 minRound length: 5, 12, 16 and 12 minute blocks
- 9 stepsRepeatable template, clarify through tests
- 3 or 4 testsHappy path, boundary, failure, injected-clock case
Show what to say
“I run every LLD round the same way: clarify, use cases, entities, public interface, diagram, code the core, then stress the design for extensibility, concurrency and tests, in about 45 minutes. I pick a pattern only when a requirement hurts, name the race before I am asked, and inject the clock so tests are deterministic.”
Trade-offs
- Pattern first vs pain first: start from the wording of the requirement and let the pain choose; a plain class is right when nothing hurts.
- Lock per room vs one global lock: lock the invariant, not the world; hold it briefly, never across slow I/O.
- In-memory lock vs database constraint: a lock protects one process; across servers a unique constraint or conditional update arbitrates.
Bring it up before they ask
Name the check-then-act race before they ask, and never say Node is single threaded: any await between the check and the act reopens the gap.
Run every round the same way: clarify, use cases, entities, interface, diagram, code the core, then stress the design, in about 45 minutes. Add a pattern only when a requirement hurts, and name the race before you are asked.
What interviewers score
An LLD round is not a pattern quiz. The rubric is about how you work.
| What they score | What it looks like | What loses points |
|---|---|---|
| Scope | Questions first | Coding from sentence one |
| Modelling | One job per class | A god class, or a class per noun |
| Working code | One use case end to end | Half of everything, nothing runs |
| Extensibility | A new requirement adds a class | Editing a switch in four files |
A 45-minute plan
The split is 5, 12, 16 and 12 minutes. The longest block is code, because only working code proves anything.
Behind schedule? Cut scope out loud: "I will code only book and cancel."
5 minutes to clarify and agree use cases, 12 to settle entities, the public interface and a class diagram, 16 to code the core, then 12 to stress the design: extensibility, concurrency and tests. Each marker is a checkpoint: if you are behind, say so and cut scope.
Text description
- Clarify: Questions, use cases (0–5 min)
- Model: Entities, interface (5–12 min), Class diagram (12–17 min)
- Code: The core, one use case end to end (17–33 min)
- Extend: One new requirement (33–38 min)
- Concurrency: Races, locks (38–42 min)
- Tests: Tests, summary (42–45 min)
The repeatable template
Clarify
Three to five questions: scope, size, failures.
Use cases
The user's verbs, as method names.
Entities
Nouns become classes, one job each. See the first lesson.
Public interface
Four to six signatures, and what each returns on failure.
Class diagram
Boxes and arrows. Five minutes.
Code the core
One use case, end to end.
Extensibility check
Aim to add a class, change none.
Concurrency
Find the check-then-act and make it one step.
Tests
Happy path, boundary, failure, injected clock.
Which pattern does this requirement call for?
Start from the wording of the requirement, not the pattern list.
No yes along the way? Plain classes are right.
Must an action be undone, queued, retried or logged as a thing?
The extensibility check
The interviewer will add a requirement, so do it to yourself first: if they add a new X, how many existing classes do I edit?
- A new vehicle type, payment method or pricing rule should be one new class and one registration line.
- A new
ifin four files means a missing seam. See the SOLID lesson. - Say it as a sentence: "To add reservations I add a
ReservationPolicy, andParkingLotdoes not change."
Practise on the parking lot exercise: after each step, name a new requirement and the class you would add.
The concurrency checklist
Every round ends with "what if two people do this at once?"
Find the check-then-act
"If it is free, take it" is a race once two callers interleave.
Make the pair one step
A lock, claim-first, or one atomic operation.
Lock the invariant, not the world
One lock per room, held briefly.
Make retries safe
Give each request an id, so a repeat returns the first result. That is .
Say what changes across servers
A memory lock covers one process. Across servers the database arbitrates.
The racy version lets both callers through. The fix claims the room in the same step as the check. "Node is single threaded, so there is no race" loses points: any await reopens the gap.
const pause = () => new Promise((resolve) => setTimeout(resolve, 0)); // stands in for a payment or database call
// Check, then act, with a gap between them. Another caller can slip into the gap.
export class RoomsRacy {
#owner = new Map();
async book(room, who) {
if (this.#owner.has(room)) return false; // check
await pause(); // anyone else may pass the same check now
this.#owner.set(room, who); // act: too late, the check is stale
return true;
}
owner(room) { return this.#owner.get(room); }
}Test with an injected clock
A test that sleeps for ten minutes never gets run. Pass in anything that reads the time, a random number or an id, so the test controls it.
The clock is a function and the test moves a variable. A hold made at time 0 is still held at 599,999 ms and gone at 600,000. Run the cell and move the clock yourself.
// The clock is a parameter, so a test can move time instead of waiting for it.
export class RoomHolds {
#until = new Map();
#now;
#holdMs;
constructor(now = Date.now, holdMs = 10 * 60 * 1000) {
this.#now = now;
this.#holdMs = holdMs;
}
isHeld(room) {
return (this.#until.get(room) ?? 0) > this.#now();
}
// A hold lasts until the exact instant start + holdMs; at that instant it is gone.
hold(room) {
if (this.isHeld(room)) return false;
this.#until.set(room, this.#now() + this.#holdMs);
return true;
}
}// The clock is a variable you control. Move it and watch the hold expire.
let t = 0; // milliseconds
const holds = new RoomHolds(() => t); // the default hold lasts 10 minutes
console.log(holds.hold('A')); // true
t = 599_999;
console.log(holds.hold('A')); // false: one millisecond left
t = 600_000;
console.log(holds.hold('A')); // true: the hold expired, so a new one starts
console.log(holds.isHeld('A')); // trueWorked example: a meeting-room booker
The prompt: "Design a system to book meeting rooms." The whole round, without the full code.
Clarify
Fixed rooms, one process, whole minutes. Out of scope: recurring meetings and calendar sync, named as follow-ups.
Use cases
book(room, start, end, who),cancel(bookingId),freeRooms(start, end).Entities
Room,Booking, andRoomBookeras the one class that owns the rules.Public interface
A clash returns
false. A backwards slot or unknown room throws. Say that aloud: it is the contract.Class diagram
RoomBookerowns rooms, and each room owns its bookings.Code the core
The subtle part is the overlap test. Use half-open intervals (start in, end out), so a meeting ending at 10 does not clash with one starting at 10.
Concurrency and tests
Two people booking one slot is the check-then-act again: check and claim in one step, with a lock per room. Across servers, a database constraint arbitrates. Test the boundary, a clash, a backwards slot, and inject the clock.
The core is book and freeRooms, short enough for a whiteboard.
// Half-open intervals [start, end): a meeting that ends at 10 does not clash with one that starts at 10.
export const overlaps = (a, b) => a.start < b.end && b.start < a.end;
export class RoomBooker {
#bookings = new Map(); // room -> list of { start, end, who }
constructor(rooms) {
for (const room of rooms) this.#bookings.set(room, []);
}
isFree(room, start, end) {
const list = this.#bookings.get(room);
if (!list) throw new Error(`unknown room: ${room}`);
return !list.some((b) => overlaps(b, { start, end }));
}
book(room, start, end, who) {
if (!(start < end)) throw new RangeError('start must be before end');
if (!this.isFree(room, start, end)) return false; // check and act in one step: no await between them
this.#bookings.get(room).push({ start, end, who });
return true;
}
freeRooms(start, end) {
return [...this.#bookings.keys()].filter((room) => this.isFree(room, start, end));
}
}When you are stuck
Everyone stalls. The recovery is mechanical:
- Say what you are stuck on. Silence reads as lost.
- Go back to the use cases. Which verb am I implementing?
- Write the simplest version: one class, a list, a loop.
- Cut scope on purpose, and say where it would plug in.
- Offer a choice, not a blank: "a lock per room or one global lock?"
Practise in this order
Work on a timer, out loud.
- Tic-tac-toe: entities and responsibilities.
- Parking lot exercise, then the question: SOLID in practice.
- Rate limiter and pricing rules: swappable, stackable behaviour.
- Vending machine and elevator: state machines.
- Event bus and text editor: observers, commands.
Follow-up questions
You are 20 minutes in and the design has grown too big. What do you do?
Stop adding. Finish the one use case that matters, and list the rest as extensions.
Your in-memory design must now run on several servers. What changes?
Memory locks protect nothing. Use a unique constraint or conditional update, and request ids for retries.
Check yourself
Question 1 of 3Score 2 out of 3 or better to mark this page complete.