Part 01 · Low Level Design
How to Solve an LLD Interview
A repeatable 45-minute plan: clarify, find the entities, draw the classes, code the core, then handle the 'what if we add…' follow-ups without a rewrite.
⏱ 1-2 hours · Station 13 of 80
Forty minutes of code, zero offers
Two candidates get the same question: "Design a meeting room booking system for our office."
Farhan starts typing straight away. By minute 35 he has 200 lines: a RoomManager with nine methods, a BookingFactory, an IRoomRepository. Then the interviewer asks, "Can two people book the same room at the same time?" Farhan doesn't know; he never asked how bookings work. Rebuilding takes the rest of the interview.
Mitu spends five minutes asking questions, two minutes listing three classes, and draws a small diagram. She codes Book and Cancel with the overlap rule in one place. When the interviewer asks about two people booking at once, she points at Book: "the check and the write go in one critical section here".
Both know OOP. Only one had a method. This last chapter of the LLD part is that method, applied end to end.
The problem: what goes wrong in LLD rounds
| Common mistake | What the interviewer sees |
|---|---|
| Coding before clarifying | Solving the wrong problem; can't handle the first follow-up |
| Ten classes for a 45-minute question | Over-engineering; no time to finish the core |
| Rules spread across classes | No clear owner; every "what if" means rewriting |
| Silent coding | No idea what you're thinking; no chance to help you |
| Patterns named, not justified | "Factory" because it sounds senior, not because a variant exists |
Think of building a house with a client watching
A good builder doesn't pour concrete the moment the client says "a house". First: how many people live here, which floors, what budget (clarify). Then the rooms and what each is for (entities). Then the floor plan (class diagram). Then building the main structure (code the core). And finally the client asks "can we add a balcony?", and the builder shows where it would go without knocking walls down (extensions).
Where the comparison breaks down. A builder has weeks; you have 45 minutes and someone evaluating how you think, not just the result. Talking through your choices out loud is part of the work.
The question is 'design a meeting room booking system'. What should your first two minutes be?
The five-step method
- Clarify (0–5 min). Ask about the actors (employees, admins), the core actions (book, cancel, view), the rules (capacity, overlap, max length), and the scale of concurrency. Say out loud what's out of scope (payments, notifications, recurring meetings, unless asked).
- Entities (5–12 min). Nouns become candidate classes; keep only those with state or behaviour. Give each one a one-line job: Room: name and capacity. Booking: room, time range, who. Scheduler: owns the rules for booking and cancelling.
- Class diagram and key methods (12–20 min). Draw boxes and relationships (chapter 3). Name the public methods:
Book(people, start, end, who),Cancel(who, start). Decide who owns each rule: the overlap check belongs toBooking, "smallest free room" toScheduler. - Code the core flow (20–38 min). Write the happy path and the main failure cases cleanly. Keep rules in their homes. Use enums and value objects where they help. Leave persistence as an in-memory slice or map.
- Extensions and follow-ups (38–45 min). Expect "what if…" questions: concurrency, a new rule, a new type. Answer by pointing at where the change would go. If your design needs a small, local change, you've shown exactly what LLD is about.
- Throughout: think out loud. Say the trade-off you're making ("I'll keep rooms sorted by capacity so the first free fit is the best fit"). Interviewers grade communication and judgement, not just code.
The meeting room question, phase by phase
From question to working Book()
Step 1/5 Pin down the rules
Rooms have a capacity. Bookings are whole hours, half-open [start, end). Two bookings in the same room can't overlap. Pick the smallest room that fits. Out of scope: recurring meetings, notifications.
- rooms
- Jamuna 2, Padma 6, Meghna 10
A checklist to keep on the whiteboard
Write these five words in a corner of the whiteboard and tick them as you go:
Clarify → Entities → Classes → Code → What-ifs.
And for every rule you discover, write next to it the class that owns it. If you can't name an owner, you've found a missing entity or a muddled responsibility.
Try it: good move or bad move?
In the interview, is this a good move?
scope: single bookings only
The meeting room scheduler in code
The result of the method: three small types, every rule in one place, clear errors, and a story that exercises each path.
package main
import (
"errors"
"fmt"
)
// Entities from the requirements: Room, Booking, and the Scheduler that owns the rules.
type Room struct {
Name string
Capacity int
}
// Booking holds a half-open time range [Start, End) in whole hours.
type Booking struct {
Room *Room
Start, End int
Who string
}
func (b Booking) Overlaps(start, end int) bool { return b.Start < end && start < b.End }
var (
ErrNoRoomFits = errors.New("no room for that many people")
ErrAllBusy = errors.New("all rooms that fit are busy")
ErrBadTime = errors.New("end must be after start")
)
type Scheduler struct {
rooms []*Room // kept smallest first, so the first free fit is the best fit
bookings []Booking
}
func (s *Scheduler) free(r *Room, start, end int) bool {
for _, b := range s.bookings {
if b.Room == r && b.Overlaps(start, end) {
return false
}
}
return true
}
// Book picks the smallest free room that fits. One method owns the whole rule.
func (s *Scheduler) Book(people, start, end int, who string) (*Room, error) {
if end <= start {
return nil, ErrBadTime
}
fits := false
for _, r := range s.rooms {
if r.Capacity < people {
continue
}
fits = true
if s.free(r, start, end) {
s.bookings = append(s.bookings, Booking{r, start, end, who})
return r, nil
}
}
if !fits {
return nil, ErrNoRoomFits
}
return nil, ErrAllBusy
}
func (s *Scheduler) Cancel(who string, start int) bool {
for i, b := range s.bookings {
if b.Who == who && b.Start == start {
s.bookings = append(s.bookings[:i], s.bookings[i+1:]...)
return true
}
}
return false
}
func main() {
s := &Scheduler{rooms: []*Room{{"Jamuna", 2}, {"Padma", 6}, {"Meghna", 10}}}
try := func(people, start, end int, who string) {
label := fmt.Sprintf("book %2d people %d-%d %-6s", people, start, end, who)
if r, err := s.Book(people, start, end, who); err != nil {
fmt.Printf("%s -> refused: %v\n", label, err)
} else {
fmt.Printf("%s -> %s (%d seats)\n", label, r.Name, r.Capacity)
}
}
try(4, 10, 11, "Nadia")
try(4, 10, 11, "Rahim")
try(12, 10, 11, "Tania")
try(4, 10, 11, "Sabbir")
try(4, 11, 12, "Sabbir")
try(2, 12, 11, "Karim")
if s.Cancel("Nadia", 10) {
fmt.Printf("%-27s -> Padma is free again\n", "cancel Nadia 10-11")
}
try(3, 10, 11, "Karim")
}from __future__ import annotations
from dataclasses import dataclass
# Entities from the requirements: Room, Booking, and the Scheduler that owns the rules.
@dataclass(frozen=True)
class Room:
name: str
capacity: int
@dataclass(frozen=True)
class Booking:
"""A half-open time range [start, end) in whole hours."""
room: Room
start: int
end: int
who: str
def overlaps(self, start: int, end: int) -> bool:
return self.start < end and start < self.end
class BookingError(Exception):
pass
class Scheduler:
def __init__(self, rooms: list[Room]):
self.rooms = sorted(rooms, key=lambda r: r.capacity) # smallest first: first free fit is best
self.bookings: list[Booking] = []
def _free(self, room: Room, start: int, end: int) -> bool:
return not any(b.room == room and b.overlaps(start, end) for b in self.bookings)
def book(self, people: int, start: int, end: int, who: str) -> Room:
"""Picks the smallest free room that fits. One method owns the whole rule."""
if end <= start:
raise BookingError("end must be after start")
fitting = [r for r in self.rooms if r.capacity >= people]
if not fitting:
raise BookingError("no room for that many people")
for room in fitting:
if self._free(room, start, end):
self.bookings.append(Booking(room, start, end, who))
return room
raise BookingError("all rooms that fit are busy")
def cancel(self, who: str, start: int) -> bool:
for b in self.bookings:
if b.who == who and b.start == start:
self.bookings.remove(b)
return True
return False
s = Scheduler([Room("Jamuna", 2), Room("Padma", 6), Room("Meghna", 10)])
def attempt(people: int, start: int, end: int, who: str) -> None:
label = f"book {people:2d} people {start}-{end} {who:<6}"
try:
r = s.book(people, start, end, who)
print(f"{label} -> {r.name} ({r.capacity} seats)")
except BookingError as e:
print(f"{label} -> refused: {e}")
attempt(4, 10, 11, "Nadia")
attempt(4, 10, 11, "Rahim")
attempt(12, 10, 11, "Tania")
attempt(4, 10, 11, "Sabbir")
attempt(4, 11, 12, "Sabbir")
attempt(2, 12, 11, "Karim")
if s.cancel("Nadia", 10):
print(f"{'cancel Nadia 10-11':<27} -> Padma is free again")
attempt(3, 10, 11, "Karim")// Entities from the requirements: Room, Booking, and the Scheduler that owns the rules.
interface Room {
readonly name: string;
readonly capacity: number;
}
// A half-open time range [start, end) in whole hours.
interface Booking {
readonly room: Room;
readonly start: number;
readonly end: number;
readonly who: string;
}
const overlaps = (b: Booking, start: number, end: number) => b.start < end && start < b.end;
class Scheduler {
readonly rooms: Room[];
bookings: Booking[] = [];
constructor(rooms: Room[]) {
this.rooms = [...rooms].sort((a, b) => a.capacity - b.capacity); // smallest first: first free fit is best
}
private free(room: Room, start: number, end: number): boolean {
return !this.bookings.some((b) => b.room === room && overlaps(b, start, end));
}
// Picks the smallest free room that fits. One method owns the whole rule.
book(people: number, start: number, end: number, who: string): Room {
if (end <= start) throw new Error("end must be after start");
const fitting = this.rooms.filter((r) => r.capacity >= people);
if (fitting.length === 0) throw new Error("no room for that many people");
const room = fitting.find((r) => this.free(r, start, end));
if (!room) throw new Error("all rooms that fit are busy");
this.bookings.push({ room, start, end, who });
return room;
}
cancel(who: string, start: number): boolean {
const i = this.bookings.findIndex((b) => b.who === who && b.start === start);
if (i < 0) return false;
this.bookings.splice(i, 1);
return true;
}
}
const s = new Scheduler([
{ name: "Jamuna", capacity: 2 },
{ name: "Padma", capacity: 6 },
{ name: "Meghna", capacity: 10 },
]);
function attempt(people: number, start: number, end: number, who: string): void {
const label = `book ${String(people).padStart(2)} people ${start}-${end} ${who.padEnd(6)}`;
try {
const r = s.book(people, start, end, who);
console.log(`${label} -> ${r.name} (${r.capacity} seats)`);
} catch (e) {
console.log(`${label} -> refused: ${(e as Error).message}`);
}
}
attempt(4, 10, 11, "Nadia");
attempt(4, 10, 11, "Rahim");
attempt(12, 10, 11, "Tania");
attempt(4, 10, 11, "Sabbir");
attempt(4, 11, 12, "Sabbir");
attempt(2, 12, 11, "Karim");
if (s.cancel("Nadia", 10)) console.log(`${"cancel Nadia 10-11".padEnd(27)} -> Padma is free again`);
attempt(3, 10, 11, "Karim");// Entities from the requirements: Room, Booking, and the Scheduler that owns the rules.
struct Room { name: &'static str, capacity: u32 }
// A half-open time range [start, end) in whole hours. `room` is an index into Scheduler.rooms.
struct Booking { room: usize, start: u32, end: u32, who: String }
impl Booking {
fn overlaps(&self, start: u32, end: u32) -> bool { self.start < end && start < self.end }
}
struct Scheduler { rooms: Vec<Room>, bookings: Vec<Booking> } // rooms kept smallest first
impl Scheduler {
fn free(&self, room: usize, start: u32, end: u32) -> bool {
!self.bookings.iter().any(|b| b.room == room && b.overlaps(start, end))
}
// Picks the smallest free room that fits. One method owns the whole rule.
fn book(&mut self, people: u32, start: u32, end: u32, who: &str) -> Result<&Room, &'static str> {
if end <= start { return Err("end must be after start"); }
let fitting: Vec<usize> = (0..self.rooms.len()).filter(|&i| self.rooms[i].capacity >= people).collect();
if fitting.is_empty() { return Err("no room for that many people"); }
let room = fitting.into_iter().find(|&i| self.free(i, start, end)).ok_or("all rooms that fit are busy")?;
self.bookings.push(Booking { room, start, end, who: who.to_string() });
Ok(&self.rooms[room])
}
fn cancel(&mut self, who: &str, start: u32) -> bool {
match self.bookings.iter().position(|b| b.who == who && b.start == start) {
Some(i) => { self.bookings.remove(i); true }
None => false,
}
}
}
fn main() {
let mut s = Scheduler {
rooms: vec![Room { name: "Jamuna", capacity: 2 }, Room { name: "Padma", capacity: 6 }, Room { name: "Meghna", capacity: 10 }],
bookings: vec![],
};
let attempt = |s: &mut Scheduler, people: u32, start: u32, end: u32, who: &str| {
let label = format!("book {people:2} people {start}-{end} {who:<6}");
match s.book(people, start, end, who) {
Ok(r) => println!("{label} -> {} ({} seats)", r.name, r.capacity),
Err(e) => println!("{label} -> refused: {e}"),
}
};
attempt(&mut s, 4, 10, 11, "Nadia");
attempt(&mut s, 4, 10, 11, "Rahim");
attempt(&mut s, 12, 10, 11, "Tania");
attempt(&mut s, 4, 10, 11, "Sabbir");
attempt(&mut s, 4, 11, 12, "Sabbir");
attempt(&mut s, 2, 12, 11, "Karim");
if s.cancel("Nadia", 10) { println!("{:<27} -> Padma is free again", "cancel Nadia 10-11"); }
attempt(&mut s, 3, 10, 11, "Karim");
}import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class Main {
// Entities from the requirements: Room, Booking, and the Scheduler that owns the rules.
record Room(String name, int capacity) {}
// A half-open time range [start, end) in whole hours.
record Booking(Room room, int start, int end, String who) {
boolean overlaps(int s, int e) { return start < e && s < end; }
}
static final class BookingException extends Exception {
private static final long serialVersionUID = 1L;
BookingException(String msg) { super(msg); }
}
static final class Scheduler {
private final List<Room> rooms;
private final List<Booking> bookings = new ArrayList<>();
Scheduler(List<Room> rooms) {
this.rooms = rooms.stream().sorted(Comparator.comparingInt(Room::capacity)).toList(); // smallest first
}
private boolean free(Room r, int start, int end) {
return bookings.stream().noneMatch(b -> b.room().equals(r) && b.overlaps(start, end));
}
// Picks the smallest free room that fits. One method owns the whole rule.
Room book(int people, int start, int end, String who) throws BookingException {
if (end <= start) throw new BookingException("end must be after start");
List<Room> fitting = rooms.stream().filter(r -> r.capacity() >= people).toList();
if (fitting.isEmpty()) throw new BookingException("no room for that many people");
for (Room r : fitting) {
if (free(r, start, end)) {
bookings.add(new Booking(r, start, end, who));
return r;
}
}
throw new BookingException("all rooms that fit are busy");
}
boolean cancel(String who, int start) {
return bookings.removeIf(b -> b.who().equals(who) && b.start() == start);
}
}
static Scheduler s = new Scheduler(List.of(new Room("Jamuna", 2), new Room("Padma", 6), new Room("Meghna", 10)));
static void attempt(int people, int start, int end, String who) {
String label = String.format("book %2d people %d-%d %-6s", people, start, end, who);
try {
Room r = s.book(people, start, end, who);
System.out.printf("%s -> %s (%d seats)%n", label, r.name(), r.capacity());
} catch (BookingException e) {
System.out.printf("%s -> refused: %s%n", label, e.getMessage());
}
}
public static void main(String[] args) {
attempt(4, 10, 11, "Nadia");
attempt(4, 10, 11, "Rahim");
attempt(12, 10, 11, "Tania");
attempt(4, 10, 11, "Sabbir");
attempt(4, 11, 12, "Sabbir");
attempt(2, 12, 11, "Karim");
if (s.cancel("Nadia", 10)) System.out.printf("%-27s -> Padma is free again%n", "cancel Nadia 10-11");
attempt(3, 10, 11, "Karim");
}
}#include <algorithm>
#include <cstdio>
#include <stdexcept>
#include <string>
#include <vector>
// Entities from the requirements: Room, Booking, and the Scheduler that owns the rules.
struct Room {
std::string name;
int capacity;
};
// A half-open time range [start, end) in whole hours. `room` indexes Scheduler::rooms_.
struct Booking {
size_t room;
int start, end;
std::string who;
bool overlaps(int s, int e) const { return start < e && s < end; }
};
struct BookingError : std::runtime_error {
using std::runtime_error::runtime_error;
};
class Scheduler {
public:
explicit Scheduler(std::vector<Room> rooms) : rooms_(std::move(rooms)) {
std::sort(rooms_.begin(), rooms_.end(), [](const Room& a, const Room& b) { return a.capacity < b.capacity; });
}
// Picks the smallest free room that fits. One method owns the whole rule.
const Room& book(int people, int start, int end, const std::string& who) {
if (end <= start) throw BookingError("end must be after start");
bool fits = false;
for (size_t i = 0; i < rooms_.size(); i++) {
if (rooms_[i].capacity < people) continue;
fits = true;
if (free(i, start, end)) {
bookings_.push_back({i, start, end, who});
return rooms_[i];
}
}
throw BookingError(fits ? "all rooms that fit are busy" : "no room for that many people");
}
bool cancel(const std::string& who, int start) {
auto it = std::find_if(bookings_.begin(), bookings_.end(),
[&](const Booking& b) { return b.who == who && b.start == start; });
if (it == bookings_.end()) return false;
bookings_.erase(it);
return true;
}
private:
bool free(size_t room, int start, int end) const {
return std::none_of(bookings_.begin(), bookings_.end(),
[&](const Booking& b) { return b.room == room && b.overlaps(start, end); });
}
std::vector<Room> rooms_; // smallest first: the first free fit is the best fit
std::vector<Booking> bookings_;
};
int main() {
Scheduler s({{"Jamuna", 2}, {"Padma", 6}, {"Meghna", 10}});
auto attempt = [&](int people, int start, int end, const std::string& who) {
char label[64];
std::snprintf(label, sizeof label, "book %2d people %d-%d %-6s", people, start, end, who.c_str());
try {
const Room& r = s.book(people, start, end, who);
std::printf("%s -> %s (%d seats)\n", label, r.name.c_str(), r.capacity);
} catch (const BookingError& e) {
std::printf("%s -> refused: %s\n", label, e.what());
}
};
attempt(4, 10, 11, "Nadia");
attempt(4, 10, 11, "Rahim");
attempt(12, 10, 11, "Tania");
attempt(4, 10, 11, "Sabbir");
attempt(4, 11, 12, "Sabbir");
attempt(2, 12, 11, "Karim");
if (s.cancel("Nadia", 10)) std::printf("%-27s -> Padma is free again\n", "cancel Nadia 10-11");
attempt(3, 10, 11, "Karim");
}All six print exactly this:
book 4 people 10-11 Nadia -> Padma (6 seats)
book 4 people 10-11 Rahim -> Meghna (10 seats)
book 12 people 10-11 Tania -> refused: no room for that many people
book 4 people 10-11 Sabbir -> refused: all rooms that fit are busy
book 4 people 11-12 Sabbir -> Padma (6 seats)
book 2 people 12-11 Karim -> refused: end must be after start
cancel Nadia 10-11 -> Padma is free again
book 3 people 10-11 Karim -> Padma (6 seats)
Reading the output:
- Nadia (4 people): Jamuna holds 2, so the smallest that fits is Padma (6).
- Rahim (4, same hour): Padma is busy 10-11, so the next fit is Meghna.
- Tania (12): no room has 12 seats: a different error from "busy", so the user knows what to change.
- Sabbir (4 at 10-11): every room that fits is busy. At 11-12 he gets Padma, because [10, 11) and [11, 12) don't overlap.
- Karim (12-11): refused before any room is checked: end must be after start.
- Cancel, then Karim (3): Padma is free again at 10-11 and is the smallest room for 3 people.
- What each language changed: Python, TypeScript, Java and C++ sort the rooms by capacity in the constructor; the Go and Rust versions expect them listed smallest first, which keeps the code shorter (say so in the interview). Rust stores a room index in each booking so the scheduler can own both lists and still return a reference to the chosen room. Java's
removeIfmakesCancelone line.
In the real interview room
- Practice the five phases on a timer. The method only works if it's automatic. Do two or three problems from the LLD Problems part with a 45-minute alarm.
- Write requirements as a short list on the board. It's your contract with the interviewer and your reference when a follow-up comes.
- Use plain names from the problem.
Room,Booking,SchedulerbeatResourceEntityandAllocationManager. - Choose the simplest data structures first (a slice, a map), and mention the better one if scale matters ("an interval tree per room if there are thousands of bookings").
- Leave 5–7 minutes for follow-ups. If you're still coding at minute 42, stop and summarise the design; an unfinished method with a clear design beats finished code nobody understands.
- It's fine to say "I'd check that". Admitting a gap and reasoning about it is better than bluffing.
Mistakes that cost the round
Pick the mistake
minute 0: type Room struct {Pause and think
At minute 40 the interviewer asks: "Now support recurring meetings: every Sunday 10-11 for 4 weeks." What do you say, without rewriting your code?
Reveal
Add a Recurrence value object (start date, weekday, count) and a method that expands it into normal bookings. Book stays as the single place that checks overlap: booking a recurring meeting means checking each expanded booking, and either booking all of them or none (so the scheduler books them inside one lock, rolling back if any one clashes). The existing classes barely change, which is the point: the rule already had one home, so the new feature reuses it.
Quick quiz
1. What are the five phases of the LLD interview method?
Answer
Clarify requirements and scope, list entities with one-line jobs, draw the class diagram and key methods, code the core flow, then handle extensions and follow-ups.
2. Why say what's out of scope?
Answer
It sets a shared contract with the interviewer, keeps the problem small enough to finish in 45 minutes, and shows you can prioritise.
3. How do you decide which class owns a rule?
Answer
Put it with the data it depends on. If it needs one object's data (overlap of two time ranges), it goes on that object; if it needs many (the smallest free room), it goes on the object that holds them all.
4. What should you do if you're still coding at minute 42?
Answer
Stop, summarise the design and the remaining steps, and leave time for follow-ups. A clear design with an unfinished method scores better than rushed code.
Your challenge: equipment
The office adds projectors. Some rooms have one.
- Give
RoomaHasProjectorfield; Padma and Meghna have one, Jamuna doesn't. - Add a
needsProjectorparameter toBook. If it's true, only rooms with a projector count as fitting. - At the end, book 2 people at 14-15 for Nusrat with a projector.
Before you run it, predict which room Nusrat gets.
You've succeeded when the new last line is book 2 people 14-15 Nusrat -> Padma (6 seats) (Jamuna fits 2 people but has no projector), every earlier line is unchanged (they all pass false), and the projector check lives inside Book only.
In one line
Clarify, list entities, sketch classes, code the core, then handle the what-ifs, out loud, in that order. Give every rule one owner so each follow-up becomes a small, local change, and spend your minutes on the core flow rather than on patterns nobody asked for.
Station stamp
Finish the chapter
Done reading? Press Complete to save your progress. XP is Pro-only — on Pro this chapter is worth +10 XP.
Go Pro to earn XP →