How low-level design interviews work
Low-level design rounds ask you to design and code the classes of a small system in about 45 minutes. This module gives you a plan for the time, shows it on a real prompt, and turns the prompt's nouns and verbs into classes and methods.
1. The idea
A low-level design (LLD) round, also called object-oriented design or machine coding, asks you to design the classes for a small system (a parking lot, an elevator, a cache) and usually to write the core of it. Where system design asks "which servers and databases?", LLD asks "which classes, which interfaces, and how do they talk?"
| Minutes | Step | What you produce |
|---|---|---|
| 0–5 | Clarify | Must do, must be, out of scope; assumptions written down |
| 5–10 | Find the nouns and verbs | Candidate classes (nouns) and methods (verbs) |
| 10–20 | Classes and relationships | A class diagram: who has whom, what varies (interfaces) |
| 20–25 | Walk the main flow | One use case traced through the objects |
| 25–40 | Code the core | The central classes, compiling or close to it |
| 40–45 | Extend and harden | A follow-up handled by adding a class; concurrency discussed |
What interviewers score: clear requirements, sensible classes with one job each, interfaces where things vary, working code for the core, and how gracefully the design absorbs a change.
2. How it works
Forty-five minutes on a notification-service prompt. Watch the whiteboard fill up step by step.
3. Try it yourself
Nouns and verbs
Click words in the prompt: once for a class (noun), twice for a method (verb), three times to clear. Then compare with a suggested answer.
4. In code
The notification service from the animation: the varying part (the channel) is an interface, and the service never names a concrete channel.
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
# 1. Entities: the nouns from the requirements
@dataclass
class User:
id: str
email: str
phone: str
channels: list[str] = field(default_factory=lambda: ["email"]) # the user's preferences
@dataclass(frozen=True)
class Template:
subject: str
body: str # uses {name}-style placeholders
def render(self, data: dict[str, str]) -> tuple[str, str]:
return self.subject.format(**data), self.body.format(**data)
# 2. The interface: what varies (the channel) goes behind it
class Channel(ABC):
name: str
@abstractmethod
def send(self, user: User, subject: str, body: str) -> str: ...
class EmailChannel(Channel):
name = "email"
def send(self, user: User, subject: str, body: str) -> str:
return f"email to {user.email}: [{subject}] {body}"
class SmsChannel(Channel):
name = "sms"
def send(self, user: User, subject: str, body: str) -> str:
return f"sms to {user.phone}: {body[:40]}"
# 3. The service: the verbs, using the interface, never a concrete channel
class NotificationService:
def __init__(self, channels: list[Channel], templates: dict[str, Template]) -> None:
self.channels = {c.name: c for c in channels}
self.templates = templates
def notify(self, user: User, event: str, data: dict[str, str]) -> list[str]:
subject, body = self.templates[event].render(data)
return [self.channels[c].send(user, subject, body) for c in user.channels if c in self.channels]
if __name__ == "__main__":
svc = NotificationService([EmailChannel(), SmsChannel()],
{"order_shipped": Template("Order {order} shipped", "Hi {name}, order {order} is on its way.")})
ana = User("u1", "ana@example.com", "+1-555-0100", ["email", "sms"])
ben = User("u2", "ben@example.com", "+1-555-0101")
for user, name in ((ana, "Ana"), (ben, "Ben")):
for line in svc.notify(user, "order_shipped", {"name": name, "order": "#42"}):
print(line)
// 1. Entities: the nouns from the requirements
interface User { id: string; email: string; phone: string; channels: string[]; } // channels: the user's preferences
class Template {
constructor(readonly subject: string, readonly body: string) {} // uses {name}-style placeholders
render(data: Record<string, string>): [string, string] {
const fill = (s: string) => s.replace(/\{(\w+)\}/g, (_, k: string) => data[k] ?? "");
return [fill(this.subject), fill(this.body)];
}
}
// 2. The interface: what varies (the channel) goes behind it
interface Channel {
readonly name: string;
send(user: User, subject: string, body: string): string;
}
class EmailChannel implements Channel {
readonly name = "email";
send(user: User, subject: string, body: string): string { return `email to ${user.email}: [${subject}] ${body}`; }
}
class SmsChannel implements Channel {
readonly name = "sms";
send(user: User, _subject: string, body: string): string { return `sms to ${user.phone}: ${body.slice(0, 40)}`; }
}
// 3. The service: the verbs, using the interface, never a concrete channel
class NotificationService {
private channels: Map<string, Channel>;
constructor(channels: Channel[], private templates: Map<string, Template>) {
this.channels = new Map(channels.map((c) => [c.name, c]));
}
notify(user: User, event: string, data: Record<string, string>): string[] {
const [subject, body] = this.templates.get(event)!.render(data);
return user.channels.filter((c) => this.channels.has(c)).map((c) => this.channels.get(c)!.send(user, subject, body));
}
}
const svc = new NotificationService([new EmailChannel(), new SmsChannel()],
new Map([["order_shipped", new Template("Order {order} shipped", "Hi {name}, order {order} is on its way.")]]));
const ana: User = { id: "u1", email: "ana@example.com", phone: "+1-555-0100", channels: ["email", "sms"] };
const ben: User = { id: "u2", email: "ben@example.com", phone: "+1-555-0101", channels: ["email"] };
for (const [user, name] of [[ana, "Ana"], [ben, "Ben"]] as [User, string][]) {
for (const line of svc.notify(user, "order_shipped", { name, order: "#42" })) console.log(line);
}import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
// 1. Entities: the nouns from the requirements
record User(String id, String email, String phone, List<String> channels) {} // channels: the user's preferences
record Template(String subject, String body) { // uses {name}-style placeholders
private static String fill(String s, Map<String, String> data) {
for (Map.Entry<String, String> e : data.entrySet()) s = s.replace("{" + e.getKey() + "}", e.getValue());
return s;
}
String[] render(Map<String, String> data) {
return new String[] {fill(subject, data), fill(body, data)};
}
}
// 2. The interface: what varies (the channel) goes behind it
interface Channel {
String name();
String send(User user, String subject, String body);
}
class EmailChannel implements Channel {
public String name() { return "email"; }
public String send(User user, String subject, String body) {
return "email to " + user.email() + ": [" + subject + "] " + body;
}
}
class SmsChannel implements Channel {
public String name() { return "sms"; }
public String send(User user, String subject, String body) {
return "sms to " + user.phone() + ": " + body.substring(0, Math.min(40, body.length()));
}
}
// 3. The service: the verbs, using the interface, never a concrete channel
class NotificationService {
private final Map<String, Channel> channels;
private final Map<String, Template> templates;
NotificationService(List<Channel> channels, Map<String, Template> templates) {
this.channels = channels.stream().collect(Collectors.toMap(Channel::name, Function.identity()));
this.templates = templates;
}
List<String> notify(User user, String event, Map<String, String> data) {
String[] msg = templates.get(event).render(data);
return user.channels().stream().filter(channels::containsKey).map(c -> channels.get(c).send(user, msg[0], msg[1])).toList();
}
}
public class Main {
public static void main(String[] args) {
NotificationService svc = new NotificationService(List.of(new EmailChannel(), new SmsChannel()),
Map.of("order_shipped", new Template("Order {order} shipped", "Hi {name}, order {order} is on its way.")));
User ana = new User("u1", "ana@example.com", "+1-555-0100", List.of("email", "sms"));
User ben = new User("u2", "ben@example.com", "+1-555-0101", List.of("email"));
for (Object[] un : new Object[][] {{ana, "Ana"}, {ben, "Ben"}})
svc.notify((User) un[0], "order_shipped", Map.of("name", (String) un[1], "order", "#42")).forEach(System.out::println);
}
}Output (the same in all three languages)
email to ana@example.com: [Order #42 shipped] Hi Ana, order #42 is on its way. sms to +1-555-0100: Hi Ana, order #42 is on its way. email to ben@example.com: [Order #42 shipped] Hi Ben, order #42 is on its way.
5. In the interview
- Say the plan out loud in the first minute: "I'll clarify, sketch the classes, walk one flow, then code the core." Interviewers relax when they can follow you.
- Write requirements where they can be seen, in three lists: must do, must be, out of scope.
- Ask "what is likely to change?" The answer tells you where interfaces go: pricing, channels, scheduling, storage.
- Code the core, not everything. Getters, setters and printing can be waved at. The interesting class (the cache, the scheduler, the state machine) must be real.
- Expect a twist near the end ("now add SMS", "now make it thread-safe"). A good design absorbs it with a new class rather than edits everywhere.
6. Remember it
How this hook works: what “Clarify, nouns, classes, flow, code, extend” means
What the slogan means, phrase by phrase
- “Clarify”
- Five minutes of questions; write the answers down.
- “nouns”
- Nouns become candidate classes; verbs become methods.
- “classes”
- Relationships and interfaces: who has whom, what varies.
- “flow”
- Trace one use case through the objects.
- “code”
- The core classes, working.
- “extend”
- Absorb a change by adding, not editing.
Why this picture fits: An architect: who lives here, which rooms, how they connect, a walk through the morning, then one room in detail, then "what if you add a floor?"
The story → the concept
| The house | The interview |
|---|---|
| Who lives here? | Clarify |
| The rooms | Classes |
| Doors between rooms | Relationships |
| A morning walk-through | The main flow |
| Drawing one room in detail | Coding the core |
| "Add a floor?" | The follow-up twist |
Watch it on an example
- Clarify: channels are email and SMS; users choose; templates have placeholders.
- Nouns: user, template, channel, notification service. Verbs: notify, render, send.
- Channel is an interface (it varies); the service depends on it; code it; then add push notifications as one new class.
The trap it saves you from
Diving into code at minute two, then rewriting it twice as requirements surface.
It still works when…
The interviewer wants only a diagram, or only code. The order still helps; spend the time where they point.
7. Common mistakes
8. Practice exercise (25 minutes)
- Pick a prompt (2 min). Design a coupon system for an online shop.
- Clarify (5 min). Write must do, must be, out of scope.
- Nouns and verbs (5 min). List candidate classes and methods; mark what varies.
- Classes (8 min). Draw the class diagram with at least one interface.
- Twist (5 min). "Now coupons can stack." What changes?
9. Self-check quiz
What's the first step of an LLD interview?
Where should interfaces go?
In a requirement, nouns usually become…
The interviewer adds a twist at minute 40. A strong design…
10. Go deeper
| Resource | Time | Why |
|---|---|---|
| Module 02: OOP essentials | Next | The building blocks the classes are made of. |
| P06: Parking lot | 45 min | The six steps on the most common prompt. |
| The system design interview framework | 15 min | The same idea one level up: servers instead of classes. |