InterviewKata
Low-level design · Module 01 of 6 · Level 1: Foundations

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

An architect sketching a house for a client doesn't start with the bricks. They ask who lives there, sketch the rooms and how they connect, walk through a morning in the house, and only then draw one room in detail. A low-level design interview is the same walk, compressed into 45 minutes.

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?"

MinutesStepWhat you produce
0–5ClarifyMust do, must be, out of scope; assumptions written down
5–10Find the nouns and verbsCandidate classes (nouns) and methods (verbs)
10–20Classes and relationshipsA class diagram: who has whom, what varies (interfaces)
20–25Walk the main flowOne use case traced through the objects
25–40Code the coreThe central classes, compiling or close to it
40–45Extend and hardenA 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.

blue = class · orange = method

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

6. Remember it

Memory hook: "Clarify, nouns, classes, flow, code, extend"Six steps in 45 minutes, in that order, with most of the time on classes and code.
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 houseThe interview
Who lives here?Clarify
The roomsClasses
Doors between roomsRelationships
A morning walk-throughThe main flow
Drawing one room in detailCoding the core
"Add a floor?"The follow-up twist

Watch it on an example

  1. Clarify: channels are email and SMS; users choose; templates have placeholders.
  2. Nouns: user, template, channel, notification service. Verbs: notify, render, send.
  3. 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

Coding immediatelyWithout requirements you'll design the wrong thing and rewrite it.
A class for every nounSome nouns are just fields (a phone number) or enums (a vehicle type). Classes need behaviour.
One giant manager classIf one class does everything, there's no design. Give each class one job.
Interfaces everywhereAbstract only what varies. Interfaces with one implementation that will never change add noise.
Ignoring the follow-upThe twist is scored. Show how the design absorbs it.

8. Practice exercise (25 minutes)

  1. Pick a prompt (2 min). Design a coupon system for an online shop.
  2. Clarify (5 min). Write must do, must be, out of scope.
  3. Nouns and verbs (5 min). List candidate classes and methods; mark what varies.
  4. Classes (8 min). Draw the class diagram with at least one interface.
  5. Twist (5 min). "Now coupons can stack." What changes?

9. Self-check quiz

What's the first step of an LLD interview?

Five minutes of questions prevent designing the wrong thing.

Where should interfaces go?

Abstract what changes: channels, pricing, scheduling.

In a requirement, nouns usually become…

Verbs become methods.

The interviewer adds a twist at minute 40. A strong design…

Open for extension, closed for modification.

10. Go deeper

ResourceTimeWhy
Module 02: OOP essentialsNextThe building blocks the classes are made of.
P06: Parking lot45 minThe six steps on the most common prompt.
The system design interview framework15 minThe same idea one level up: servers instead of classes.