Object-Oriented Programming & Low-Level Design

A FAANG interview preparation guide: OOP pillars, SOLID principles, 12 GoF patterns, UML, and 9 classic LLD problems with staged solutions.

8 Modules • 24 Visualizations • 60+ Code Samples • Java + Python + TypeScript

Module 01

OOP Pillars

The four conceptual foundations — encapsulation, inheritance, polymorphism, abstraction — plus the mechanics of static vs. dynamic dispatch.

Encapsulation

Encapsulation bundles state (fields) and the behavior that operates on that state (methods) inside a single unit, and controls access through a narrow public interface. The goal is to make illegal states unrepresentable and illegal transitions unreachable from outside the object.

Two independent ideas, often conflated

  • Bundling: data + behavior live together in the class.
  • Information hiding: internal representation is private; clients depend only on the interface. Swapping ArrayList for LinkedList in a private field should break nothing.

Java example — BankAccount

public final class BankAccount {
    private long balanceCents;         // invariant: >= 0
    private final String owner;

    public BankAccount(String owner, long openingCents) {
        if (openingCents < 0) throw new IllegalArgumentException("negative opening");
        this.owner = Objects.requireNonNull(owner);
        this.balanceCents = openingCents;
    }
    public long balanceCents() { return balanceCents; }
    public synchronized void deposit(long cents) {
        if (cents <= 0) throw new IllegalArgumentException();
        balanceCents += cents;                      // invariant preserved
    }
    public synchronized void withdraw(long cents) {
        if (cents <= 0 || cents > balanceCents) throw new IllegalStateException();
        balanceCents -= cents;
    }
}

Python equivalent

class BankAccount:
    def __init__(self, owner: str, opening_cents: int) -> None:
        if opening_cents < 0: raise ValueError("negative opening")
        self._owner = owner                 # leading underscore = "private by convention"
        self._balance_cents = opening_cents
    @property
    def balance_cents(self) -> int: return self._balance_cents
    def deposit(self, cents: int) -> None:
        if cents <= 0: raise ValueError
        self._balance_cents += cents
    def withdraw(self, cents: int) -> None:
        if cents <= 0 or cents > self._balance_cents: raise RuntimeError
        self._balance_cents -= cents

Inheritance

Inheritance models "is-a" — a subclass specializes a superclass and inherits its members. It exists to enable subtype polymorphism (the subclass can be used wherever the superclass is expected) and to reuse implementation.

The two axes

  • Subtyping (interface): Dog is-a Animal at the type level; the compiler lets you pass a Dog where Animal is required.
  • Subclassing (implementation): fields and method bodies of the superclass are re-used by the subclass.

Java bundles both; Go separates them (interfaces provide subtyping; struct embedding provides implementation reuse).

Java snippet

abstract class Shape {
    abstract double area();
    public String describe() { return getClass().getSimpleName() + " area=" + area(); }
}
class Circle extends Shape {
    private final double r;
    public Circle(double r) { this.r = r; }
    @Override double area() { return Math.PI * r * r; }
}
class Rectangle extends Shape {
    private final double w, h;
    public Rectangle(double w, double h) { this.w = w; this.h = h; }
    @Override double area() { return w * h; }
}

Python snippet

from abc import ABC, abstractmethod
class Shape(ABC):
    @abstractmethod
    def area(self) -> float: ...
    def describe(self) -> str: return f"{type(self).__name__} area={self.area()}"
class Circle(Shape):
    def __init__(self, r: float): self.r = r
    def area(self) -> float: return 3.14159 * self.r ** 2
class Rectangle(Shape):
    def __init__(self, w: float, h: float): self.w, self.h = w, h
    def area(self) -> float: return self.w * self.h

Polymorphism

Polymorphism = "one interface, many implementations." Four flavors in practice:

KindResolved atJava examplePython example
Subtype (dynamic dispatch)runtimeshape.area()shape.area()
Ad-hoc (overloading)compile-timemax(int,int) / max(double,double)n/a (single method)
Parametric (generics)compile-timeList<T>duck-typed / TypeVar
Coercion (implicit conversion)compile-timeint -> longint -> float

Java — subtype polymorphism in action

public static double totalArea(List<Shape> shapes) {
    double total = 0;
    for (Shape s : shapes) total += s.area();   // dispatches on s's runtime class
    return total;
}
// callers do not know or care whether s is a Circle, Rectangle, or a type invented tomorrow.

Abstraction

Abstraction exposes essential behavior while hiding implementation. In Java it is expressed through interface and abstract class; in Python through abstract base classes (abc) or protocols (typing.Protocol).

Abstract class vs. interface

Aspectabstract classinterface
StateCan hold fieldsOnly constants (Java)
MethodsAbstract + concreteAbstract + default (since Java 8)
InheritanceSingle (Java)Multiple
Use whenShared state + behavior across related typesCapability shared by unrelated types (Comparable)

Python Protocol (structural typing)

from typing import Protocol
class Drawable(Protocol):
    def draw(self, canvas) -> None: ...

def render(items: list[Drawable], canvas) -> None:
    for item in items: item.draw(canvas)     # any object with .draw() works

Class Diagram Legend

Every LLD interview ends with you drawing UML on the whiteboard. Know the connectors.

UML Relationship Cheat Tree

Multiplicity notation

  • 1 — exactly one
  • 0..1 — zero or one (optional)
  • * or 0..* — many
  • 1..* — at least one
  • n..m — bounded range

Static vs Dynamic Dispatch

Method dispatch is the runtime's mechanism for choosing which implementation runs when you call obj.method(). Static dispatch binds at compile time (by declared type); dynamic dispatch binds at runtime (by actual object class).

Why it matters

  • Static: one assembly jump, branch predictor-friendly, inlinable.
  • Dynamic: an extra indirection through the vtable (Java/C++), method resolution order (Python), or itable (Go interface).
Dispatch resolution — animal.speak() on a Dog instance
Module 02

SOLID Principles

Robert C. Martin's five design heuristics that turn "code that works" into "code that survives contact with change". Each principle shown with before/after across Java, Python, and TypeScript.

SOLID at a glance

S — Single Responsibility Principle

A class should have one, and only one, reason to change. "Responsibility" here means source of change — an actor (business role, subsystem, stakeholder) who can request modifications. Two responsibilities => two reasons to change => a class that mutates whenever either actor speaks up.

Before — one class, three reasons to change

class Employee {
    double calculatePay()           { /* finance says: new bonus formula */ }
    void   save(Connection db)      { /* DBA says: switch to postgres */ }
    String reportHours()            { /* HR says: add overtime column */ }
}

After — one concern per class

class Employee { /* data + invariants only */ }
class PayCalculator  { double payFor(Employee e) { ... } }
class EmployeeRepo   { void save(Employee e)    { ... } }
class HoursReporter  { String report(Employee e)  { ... } }

O — Open/Closed Principle

Software entities should be open for extension, closed for modification. Adding a new kind of shape, payment, or sensor should require adding new code, not editing an if/else chain in existing code. Achieved by dispatching through an abstraction.

Before — switch must be edited for every new shape

class AreaCalculator {
    double area(Object s) {
        if (s instanceof Circle c)     return Math.PI * c.r * c.r;
        if (s instanceof Rectangle r)  return r.w * r.h;
        throw new IllegalArgumentException();
    }
}

After — new shape ships in its own file

interface Shape { double area(); }
class Circle    implements Shape { ... }
class Rectangle implements Shape { ... }
class Triangle  implements Shape { ... }   // added, nothing else changed
class AreaCalculator { double area(Shape s) { return s.area(); } }

L — Liskov Substitution Principle

If S is a subtype of T, any T in a program can be replaced with an S without altering correctness. Expressed as the "square/rectangle" trap: a Square that inherits from Rectangle violates LSP because setting width independently breaks the parent's contract.

Before — LSP violated

class Rectangle { int w, h; void setW(int v){w=v;} void setH(int v){h=v;} int area(){return w*h;} }
class Square extends Rectangle {
    @Override void setW(int v) { w = h = v; }
    @Override void setH(int v) { w = h = v; }
}
// Caller expects Rectangle contract: setW(5);setH(4); -> area=20. Square breaks it.

After — neither is-a each other; share Shape instead

interface Shape { int area(); }
record Rectangle(int w, int h) implements Shape { public int area() { return w*h; } }
record Square(int side)        implements Shape { public int area() { return side*side; } }

I — Interface Segregation Principle

Clients should not be forced to depend on methods they do not use. Fat interfaces leak irrelevant concerns into every implementer. Split by client need.

Before — one fat interface

interface Worker {
    void work();
    void eat();           // robots should not care about lunch
    void sleep();
}

After — role interfaces

interface Workable { void work(); }
interface Feedable { void eat(); }
interface Restable { void sleep(); }
class Human implements Workable, Feedable, Restable { ... }
class Robot implements Workable { ... }

D — Dependency Inversion Principle

High-level modules should not depend on low-level modules; both should depend on abstractions. The direction of source-code dependencies should point away from concrete details, toward policy.

Before — policy class hard-codes an implementation

class OrderService {
    private MySqlOrderRepo repo = new MySqlOrderRepo();   // bound to MySQL forever
    void place(Order o) { repo.save(o); }
}

After — invert the dependency

interface OrderRepo { void save(Order o); }           // owned by the policy layer
class OrderService {
    private final OrderRepo repo;
    public OrderService(OrderRepo repo) { this.repo = repo; }
    void place(Order o) { repo.save(o); }
}
// Low-level implementations live downstream:
class MySqlOrderRepo    implements OrderRepo { ... }
class InMemoryOrderRepo implements OrderRepo { ... }  // test double
Module 03

GoF Design Patterns

Twelve high-yield patterns from the Gang of Four catalogue. For each: intent, UML, Java + Python code, anti-pattern, and when NOT to reach for it.

Singleton (Creational)

Intent: ensure a class has exactly one instance and provide a global access point. Legitimate when the resource is truly one-of-a-kind (config, logger, connection pool).

Singleton UML
// Enum singleton — serialization/reflection-safe, thread-safe by JVM guarantee.
public enum Config {
    INSTANCE;
    private final Properties props = load();
    public String get(String k) { return props.getProperty(k); }
    private Properties load() { ... }
}
// Usage: Config.INSTANCE.get("db.url");

Factory Method (Creational)

Intent: defer instantiation to subclasses — the factory method returns an abstract product, and each subclass picks the concrete variant.

Factory Method UML
abstract class Dialog {
    abstract Button createButton();           // factory method
    public void render() {
        Button b = createButton();
        b.onClick(this::close);
        b.render();
    }
}
class WindowsDialog extends Dialog {
    @Override Button createButton() { return new WindowsButton(); }
}
class WebDialog extends Dialog {
    @Override Button createButton() { return new HtmlButton(); }
}

Abstract Factory (Creational)

Intent: provide an interface for creating families of related objects without specifying their concrete classes. Classic example: cross-platform UI kit where Button and Checkbox must come from the same family (Windows or Mac).

Abstract Factory UML
interface GuiFactory {
    Button   createButton();
    Checkbox createCheckbox();
}
class WinFactory implements GuiFactory {
    public Button createButton()    { return new WinButton(); }
    public Checkbox createCheckbox(){ return new WinCheckbox(); }
}
class MacFactory implements GuiFactory {
    public Button createButton()    { return new MacButton(); }
    public Checkbox createCheckbox(){ return new MacCheckbox(); }
}
class App {
    private final GuiFactory f;
    App(GuiFactory f) { this.f = f; }
    void render() { f.createButton().render(); f.createCheckbox().render(); }
}

Builder (Creational)

Intent: separate construction of a complex object from its representation, so the same construction process can yield different outcomes and long constructors vanish. Use when an object has 4+ optional fields.

Builder UML
public final class HttpRequest {
    private final String url; private final String method;
    private final Map<String,String> headers; private final byte[] body;
    private HttpRequest(Builder b) {
        this.url=b.url; this.method=b.method;
        this.headers=Map.copyOf(b.headers); this.body=b.body;
    }
    public static Builder get(String url)  { return new Builder(url, "GET"); }
    public static Builder post(String url) { return new Builder(url, "POST"); }
    public static class Builder {
        private final String url, method;
        private Map<String,String> headers = new LinkedHashMap<>();
        private byte[] body;
        Builder(String u, String m){ url=u; method=m; }
        public Builder header(String k, String v){ headers.put(k,v); return this; }
        public Builder body(byte[] b){ body=b; return this; }
        public HttpRequest build(){ return new HttpRequest(this); }
    }
}
// Usage: HttpRequest.post("/api").header("X","1").body(bytes).build();

Prototype (Creational)

Intent: create new objects by copying a prototype instance. Useful when construction is expensive (e.g., heavy initialization, complex state graphs) or when the class of objects to create is decided at runtime.

Prototype UML
interface Shape extends Cloneable { Shape deepCopy(); }
class Circle implements Shape {
    double cx, cy, r; Color fill;
    public Circle deepCopy() {
        Circle c = new Circle();
        c.cx = cx; c.cy = cy; c.r = r;
        c.fill = new Color(fill.rgb);     // deep copy mutable fields
        return c;
    }
}
class ShapeRegistry {
    private final Map<String,Shape> map = new HashMap<>();
    public void register(String name, Shape proto) { map.put(name, proto); }
    public Shape create(String name) { return map.get(name).deepCopy(); }
}

Adapter (Structural)

Intent: convert the interface of a class into another interface clients expect. The "shape of a plug" problem: you have a legacy library you cannot change but your callers speak a different protocol.

Adapter UML (object adapter)
interface Logger {                                // Target — what we want
    void info(String msg);
}
class LegacyLog {                                  // Adaptee — what we have
    public void write(int level, String text) { ... }
}
class LegacyLogAdapter implements Logger {         // Adapter
    private final LegacyLog legacy;
    public LegacyLogAdapter(LegacyLog l){ legacy = l; }
    public void info(String msg){ legacy.write(1, msg); }
}

Decorator (Structural)

Intent: attach new responsibilities to an object dynamically by wrapping it in another object with the same interface. Good for cross-cutting concerns — caching, logging, auth, retries — without subclass explosions.

Decorator UML
interface Notifier { void send(String msg); }
class EmailNotifier implements Notifier { public void send(String m){ ... } }
abstract class NotifierDecorator implements Notifier {
    protected final Notifier wrappee;
    NotifierDecorator(Notifier w){ wrappee = w; }
    public void send(String m){ wrappee.send(m); }
}
class SmsNotifier extends NotifierDecorator {
    SmsNotifier(Notifier w){ super(w); }
    @Override public void send(String m){ super.send(m); sendSms(m); }
}
// new SmsNotifier(new EmailNotifier()).send("hi")  sends both.

Proxy (Structural)

Intent: provide a surrogate that controls access to the real subject. Types: virtual (lazy-load), remote (RPC stub), protection (auth), caching.

Proxy UML
interface ImageService { Image load(String url); }
class RealImageService implements ImageService {
    public Image load(String url) { /* expensive HTTP + decode */ }
}
class CachingImageProxy implements ImageService {
    private final ImageService inner;
    private final Map<String,Image> cache = new ConcurrentHashMap<>();
    public CachingImageProxy(ImageService i) { inner = i; }
    public Image load(String url) {
        return cache.computeIfAbsent(url, inner::load);
    }
}

Observer (Behavioral)

Intent: establish a one-to-many dependency so that when one object (subject) changes state, all its dependents (observers) are notified. The backbone of pub/sub, UI event systems, reactive streams.

Observer UML
interface Observer<E> { void update(E event); }
class Subject<E> {
    private final List<Observer<E>> obs = new CopyOnWriteArrayList<>();
    public void subscribe(Observer<E> o)   { obs.add(o); }
    public void unsubscribe(Observer<E> o) { obs.remove(o); }
    protected void publish(E e)            { for (var o: obs) o.update(e); }
}
class PriceFeed extends Subject<Quote> {
    public void onTick(Quote q){ publish(q); }
}

Strategy (Behavioral)

Intent: define a family of algorithms, encapsulate each, and make them interchangeable at runtime. The Comparator you pass to Collections.sort is a Strategy.

Strategy UML
interface CompressionStrategy { byte[] compress(byte[] data); }
class GzipStrategy  implements CompressionStrategy { ... }
class BrotliStrategy implements CompressionStrategy { ... }

class Archiver {
    private CompressionStrategy strategy;
    public void setStrategy(CompressionStrategy s){ strategy = s; }
    public byte[] archive(byte[] bytes){ return strategy.compress(bytes); }
}

Command (Behavioral)

Intent: encapsulate a request as an object so you can queue it, log it, undo it, or send it over the wire. Backbone of undo/redo, task queues, macro recording.

Command UML
interface Command { void execute(); void undo(); }
class InsertText implements Command {
    private final Editor editor; private final int pos; private final String text;
    public InsertText(Editor e, int p, String t){ editor=e; pos=p; text=t; }
    public void execute(){ editor.insert(pos, text); }
    public void undo()   { editor.delete(pos, text.length()); }
}
class History {
    private final Deque<Command> stack = new ArrayDeque<>();
    public void run(Command c){ c.execute(); stack.push(c); }
    public void undo(){ if (!stack.isEmpty()) stack.pop().undo(); }
}

State (Behavioral)

Intent: allow an object to alter its behavior when its internal state changes — the object appears to change class. Swap giant if (state == X) ... else if (state == Y) ... chains with polymorphic state objects.

State UML
interface OrderState {
    void pay(Order o); void ship(Order o); void cancel(Order o);
}
class Created implements OrderState {
    public void pay(Order o)   { o.setState(new Paid()); }
    public void ship(Order o)  { throw new IllegalStateException(); }
    public void cancel(Order o){ o.setState(new Cancelled()); }
}
class Paid implements OrderState { /* pay -> error; ship -> Shipped; cancel -> Refunding */ }
class Order {
    private OrderState state = new Created();
    public void setState(OrderState s){ state = s; }
    public void pay(){ state.pay(this); }
    public void ship(){ state.ship(this); }
    public void cancel(){ state.cancel(this); }
}
Module 04

UML Crash Course

Four diagram types are enough for 99% of LLD interviews: class, sequence, state, activity.

Class Diagrams

A class box has three compartments: name / attributes / operations. Visibility markers: + public, - private, # protected, ~ package. Static members are underlined.

Notation legend

ArrowMeaningSemantics
Solid with hollow triangle ◁—GeneralizationSubclass extends superclass
Dashed with hollow triangle ◁––RealizationClass implements interface
Solid with filled diamond ⯁—CompositionWhole owns part; shared lifetime
Solid with hollow diamond ◇—AggregationWhole has part; part may outlive whole
Solid line —AssociationStable reference between two classes
Dashed arrow ––>DependencyTransient use (parameter, local var)

Sequence Diagrams

Time flows top to bottom; participants line up along the top. Solid arrows = synchronous call; dashed = return; open arrow = asynchronous.

Sequence: Client -> Service -> DB (cache miss / hit)

State Diagrams

Circles = states; arrows = transitions labelled event [guard] / action. Solid dot = initial state; bulls-eye = terminal state.

Example: Order lifecycle
[*] --> Created
Created --> Paid       : pay() [amount==total]
Created --> Cancelled  : cancel()
Paid    --> Shipped    : ship() / decrement stock
Paid    --> Refunding  : cancel()
Shipped --> Delivered  : confirmDelivery()
Shipped --> Returning  : initiateReturn()
Delivered --> [*]
Cancelled --> [*]

Activity Diagrams

Flowchart for a process. Rounded rectangles = activities; diamonds = decisions; bars = fork/join for parallel paths. Use when a sequence of steps has branching but no object lifecycle.

Example: Checkout
Start -> validateCart -> [valid?]
  yes -> authorizePayment -> [auth?]
    yes -> reserveInventory -> shipOrder -> End
    no  -> showError -> End
  no  -> showError -> End
Module 05

LLD Problems

Nine classic design problems. Each shows requirements, class model, key sequence, thread-safety, and code for the hardest method.

Parking Lot

Requirements

  • Multi-floor lot; each floor has slots of types: Compact, Large, Handicapped, Motorcycle.
  • Entry/exit gates; take ticket on entry, compute fee on exit.
  • Hourly pricing, with different rates per slot type.
  • Support multiple parking strategies (nearest, least-used-floor, random).
  • Concurrent park/unpark from many gates; no double-allocation of the same slot.
Parking Lot — class model
Parking Lot — park() sequence
// Hardest method: atomic allocation across concurrent gates.
public Optional<Ticket> park(Vehicle v) {
    for (Floor f : floors) {                     // try each floor
        Optional<Slot> s = strategy.pick(f.freeSlotsFor(v.type()));
        if (s.isEmpty()) continue;
        if (s.get().claim()) {                    // CAS isFree: true->false
            Ticket t = new Ticket(UUID.randomUUID(), v, s.get(), Instant.now());
            ticketRepo.save(t);
            return Optional.of(t);
        }
        // Lost the race — another gate got it; loop and retry.
    }
    return Optional.empty();                      // lot full
}

Elevator System

Requirements

  • N elevators and M floors; both hall (outside) calls and cab (inside) requests.
  • Scheduling must minimize total wait / travel; support directions (UP/DOWN/IDLE).
  • Handle SCAN/LOOK-like algorithms (elevator continues in current direction, reverses only when no more pending in that direction).
  • Emergency stop + maintenance mode remove an elevator from the pool.
Elevator — class model
Elevator — dispatch sequence
// Hardest method: SCAN/LOOK scoring for dispatcher.
int scoreFor(Elevator e, Request r) {
    int d = Math.abs(e.floor() - r.srcFloor());
    if (e.dir() == Direction.IDLE)                   return d;
    boolean sameDir = e.dir() == r.dir();
    boolean onWay = sameDir &&
        ((e.dir() == UP   && e.floor() <= r.srcFloor()) ||
         (e.dir() == DOWN && e.floor() >= r.srcFloor()));
    return onWay ? d : (MAX_FLOOR * 2) + d;       // punish out-of-direction calls
}

Splitwise

Requirements

  • Users create Groups; Groups own Expenses; Expenses split amongst members (equal, percentage, exact).
  • Maintain per-pair balance sheet; simplify debts (A owes B, B owes C => A owes C).
  • Settle-up records a payment that zeroes a pair's balance.
  • All amounts in minor units (cents) to avoid floating-point drift.
Splitwise — class model
// Hardest method: equal-split with remainder distribution (integer math).
public Map<User,Long> equalSplit(long cents, List<User> people) {
    long base = cents / people.size();
    long rem  = cents % people.size();                // leftover pennies
    Map<User,Long> shares = new LinkedHashMap<>();
    for (int i = 0; i < people.size(); i++) {
        long extra = i < rem ? 1 : 0;              // first `rem` members get 1 cent more
        shares.put(people.get(i), base + extra);
    }
    return shares;                                    // sum == cents, guaranteed
}

Tic-Tac-Toe

Requirements

  • NxN board (usually 3); two players alternate placing marks.
  • Detect win (row/col/diag of N), draw, illegal move.
  • Extensible to other 2-player board games (pluggable Rules).
Tic-Tac-Toe — class model
class Board {
    private final int n;
    private final Mark[][] cells;
    private final int[] rowSum, colSum;   // +1 for X, -1 for O
    private int diag, antiDiag;
    Board(int n){ this.n=n; cells = new Mark[n][n]; rowSum=new int[n]; colSum=new int[n]; }

    Mark place(int r, int c, Mark m) {
        if (cells[r][c] != null) throw new IllegalStateException("taken");
        cells[r][c] = m;
        int delta = m == Mark.X ? 1 : -1;
        rowSum[r] += delta; colSum[c] += delta;
        if (r == c)          diag     += delta;
        if (r + c == n - 1)  antiDiag += delta;
        if (Math.abs(rowSum[r]) == n || Math.abs(colSum[c]) == n
              || Math.abs(diag) == n || Math.abs(antiDiag) == n) return m;
        return Mark.EMPTY;          // EMPTY signals "no win yet"
    }
}

LRU Cache

Requirements

  • Fixed capacity; evict the least-recently-used entry on overflow.
  • get(k) and put(k,v) both O(1) expected.
  • Thread-safe variant for concurrent workloads.
LRU — class model
public class LRUCache<K,V> {
    private static class Node<K,V> { K k; V v; Node<K,V> prev, next; }
    private final int cap;
    private final Map<K,Node<K,V>> map = new HashMap<>();
    private final Node<K,V> head = new Node<>(), tail = new Node<>();
    public LRUCache(int cap) { this.cap = cap; head.next = tail; tail.prev = head; }

    public synchronized V get(K k) {
        Node<K,V> n = map.get(k);
        if (n == null) return null;
        moveToFront(n);
        return n.v;
    }
    public synchronized void put(K k, V v) {
        Node<K,V> n = map.get(k);
        if (n != null) { n.v = v; moveToFront(n); return; }
        if (map.size() == cap) {
            Node<K,V> lru = tail.prev;
            unlink(lru); map.remove(lru.k);
        }
        Node<K,V> nn = new Node<>(); nn.k = k; nn.v = v;
        linkFront(nn); map.put(k, nn);
    }
    private void unlink(Node<K,V> n)   { n.prev.next = n.next; n.next.prev = n.prev; }
    private void linkFront(Node<K,V> n){ n.next = head.next; n.prev = head; head.next.prev = n; head.next = n; }
    private void moveToFront(Node<K,V> n){ unlink(n); linkFront(n); }
}

Rate Limiter

Requirements

  • Per-key (user, API key, IP) rate ceiling. Common shapes: fixed window, sliding window, token bucket, leaky bucket.
  • Allow bursts up to capacity, then smooth to the refill rate (token bucket).
  • Clock must be injectable for testing.
Rate Limiter — class model
// Token bucket — nano-time refill, no background thread.
public class TokenBucket {
    private final double capacity, refillPerNs;
    private double tokens;
    private long lastNs;
    private final LongSupplier clock;
    public TokenBucket(double cap, double rps, LongSupplier clock) {
        this.capacity = cap; this.refillPerNs = rps / 1e9;
        this.tokens = cap; this.clock = clock; this.lastNs = clock.getAsLong();
    }
    public synchronized boolean tryAcquire(double cost) {
        long now = clock.getAsLong();
        double add = (now - lastNs) * refillPerNs;
        tokens = Math.min(capacity, tokens + add);
        lastNs = now;
        if (tokens >= cost) { tokens -= cost; return true; }
        return false;
    }
}

Notification Service

Requirements

  • Send notifications across Email, SMS, Push, Webhook channels.
  • Per-channel throttling + retry with exponential backoff + DLQ.
  • Templates parameterised by locale; formatted per channel.
  • Bulk send (fan-out); idempotent by (userId, eventId, channel).
Notification Service — class model
Notification — fan-out sequence
// Exponential backoff with jitter.
long backoffMs(int attempt) {
    long base = Math.min(30_000, 500L * (1L << attempt));   // 500,1000,2000,... capped
    return ThreadLocalRandom.current().nextLong(base / 2, base);  // full jitter
}

BookMyShow

Requirements

  • Cities → Cinemas → Halls → Shows (movie + time + hall).
  • Each show has a seat map; users can hold seats for N minutes, then confirm or release.
  • Payment integration; idempotent booking.
  • No two users can book the same seat (strong consistency on seat state).
BookMyShow — class model
BookMyShow — seat hold + confirm
// Hold N seats atomically; all-or-nothing.
public boolean hold(List<String> seatIds, String user, Duration ttl) {
    List<String> claimed = new ArrayList<>();
    try {
        for (String id : seatIds) {
            if (!redis.setIfAbsent("seat:" + id, user, ttl)) {
                throw new SeatUnavailableException(id);
            }
            claimed.add(id);
        }
        return true;
    } catch (SeatUnavailableException e) {
        for (String id : claimed) redis.deleteIfValue("seat:" + id, user);
        return false;                        // rollback partial holds
    }
}

Library Management

Requirements

  • Catalog of Books; multiple physical BookItems per Book.
  • Members borrow, return, reserve; max N concurrent loans per member.
  • Late fees accrue per day past due.
  • Search by title/author/category.
Library — class model
// Borrow: atomic status flip + loan creation.
public Loan borrow(Member m, BookItem item) {
    if (m.openLoans() >= MAX_LOANS) throw new LoanLimitException();
    if (!item.tryCheckout()) throw new ItemUnavailableException();
    Loan loan = new Loan(m.id(), item.barcode(), LocalDate.now().plusDays(14));
    loanRepo.save(loan);
    return loan;
}
public Money computeFine(Loan l, LocalDate today) {
    long over = ChronoUnit.DAYS.between(l.dueDate(), today);
    return over <= 0 ? Money.ZERO : Money.cents(over * FINE_CENTS_PER_DAY);
}
Module 06

Concurrency in LLD

Thread-safety patterns every LLD interview touches: thread-safe singleton, producer-consumer, and a correct bounded blocking queue.

Thread-Safe Singleton

Three contenders; only two are correct under the Java Memory Model (JMM).

1. Enum singleton (best)

public enum Config { INSTANCE;
    private final Properties p = load();
    public String get(String k){ return p.getProperty(k); }
}
// JVM guarantees enum constants are initialized exactly once + thread-safe.

2. Initialization-on-demand holder

public class Config {
    private Config() {}
    private static class Holder { static final Config INSTANCE = new Config(); }
    public static Config get() { return Holder.INSTANCE; }
    // Class loader enforces one-time init under the JMM.
}

3. Double-checked locking (only with volatile)

public class Config {
    private static volatile Config INSTANCE;   // MUST be volatile
    public static Config get() {
        Config local = INSTANCE;                // read once
        if (local == null) {
            synchronized (Config.class) {
                local = INSTANCE;
                if (local == null) INSTANCE = local = new Config();
            }
        }
        return local;
    }
}

Producer-Consumer on a Bounded Buffer

One of the first concurrency questions asked. Producer thread(s) fill a buffer; consumer thread(s) drain it; both block when the buffer is full/empty.

Bounded buffer — producer & consumer handshake
public class BoundedBuffer<T> {
    private final Object[] buf;
    private int head, tail, size;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull  = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    public BoundedBuffer(int cap){ buf = new Object[cap]; }

    public void put(T v) throws InterruptedException {
        lock.lock();
        try {
            while (size == buf.length) notFull.await();   // spin-safe: re-check
            buf[tail] = v; tail = (tail + 1) % buf.length; size++;
            notEmpty.signal();
        } finally { lock.unlock(); }
    }
    @SuppressWarnings("unchecked")
    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (size == 0) notEmpty.await();
            T v = (T) buf[head]; buf[head] = null;
            head = (head + 1) % buf.length; size--;
            notFull.signal();
            return v;
        } finally { lock.unlock(); }
    }
}

Blocking Queue API

Extends the buffer with timeouts and try-variants — the standard BlockingQueue contract.

MethodBehavior on full (put) / empty (take)
add / removeThrows exception
offer / pollReturns false/null
put / takeBlocks indefinitely
offer(v,t,u) / poll(t,u)Blocks up to timeout, then returns
Module 07

Testing

Enough testing vocabulary and technique to handle "how would you test this?" for any LLD answer.

Mocks, Stubs, Fakes, Spies

DoubleDefinitionExampleWhen to use
DummyObject passed but never usedA null loggerSatisfy a required argument
StubReturns canned answersclock.now() = 2024-01-01Drive the code under test down a branch
FakeWorking but lightweight implementationIn-memory repositoryReplace slow dependency (DB, network)
SpyRecords calls for later assertionCapture emailer.send argsVerify interactions
MockPre-programmed with expectations; fails test if not metMockito verify()Assert exact protocol between two objects
// Python fake vs stub
class FakeRepo:                     # fake: actually stores data
    def __init__(self): self.rows = []
    def save(self, r): self.rows.append(r)
    def find(self, id): return next((r for r in self.rows if r.id == id), None)

class StubClock:                    # stub: returns canned value
    def __init__(self, t): self.t = t
    def now(self): return self.t

Dependency Injection Patterns

  • Constructor injection (preferred): dependencies are required, immutable, and visible in the signature.
  • Setter injection: optional dependencies; avoid for core collaborators (risk of half-constructed objects).
  • Method injection: pass the dependency per call; handy for short-lived contexts.
  • Service locator (anti): class asks a global registry for its dependencies — hides coupling. Prefer explicit DI.
// Good — constructor injection. Test sets up doubles; production wires real ones.
public class OrderService {
    private final OrderRepo repo;
    private final PaymentGateway pg;
    private final Clock clock;
    public OrderService(OrderRepo repo, PaymentGateway pg, Clock clock) {
        this.repo = repo; this.pg = pg; this.clock = clock;
    }
}

Test Pyramid

Martin Fowler's pyramid: lots of fast unit tests, fewer integration tests, very few end-to-end tests. Inverting it ("ice-cream cone") yields slow, flaky suites.

E2E (few, slow) Integration Unit tests (many, fast) more coverage → higher realism ↑
Figure 7.1 — Healthy suite: broad base of unit tests, thin tip of E2E.
Module 08

Cheat Sheet

Pattern-vs-use-case matrix and one-liners you can rehearse the night before.

GoF pattern taxonomy

Pattern vs Use-Case Matrix

ScenarioPattern(s) to name-dropWhy
Global config / loggerSingleton (enum)Process-wide single instance.
Swap algorithm at runtimeStrategyPlug-in behavior chosen by caller.
Undo/redo, queues, macrosCommandRequests as objects support history.
UI frameworks; event busesObserverDecoupled 1-to-many notification.
Build complex immutable objectsBuilderEliminates telescoping constructors.
Object lifecycle with transitionsStateAvoid if/else; each state knows its rules.
Wrap third-party APIAdapterReshape to expected interface.
Add caching/auth/loggingDecorator, ProxyLayer behavior on the same interface.
Cross-platform UI (widgets)Abstract FactorySwap families consistently.
Expensive / dynamic instantiationPrototypeClone existing rather than rebuild.
Parallel class hierarchiesFactory MethodSubclass decides product.

Rapid Reference

SOLID one-liners

  • SRP: one reason to change
  • OCP: add code, not edit it
  • LSP: subtypes honor parent contract
  • ISP: split fat interfaces by client
  • DIP: depend on abstractions

LLD interview rhythm

  • Clarify requirements (functional + non-functional)
  • Identify core entities & relationships (draw UML)
  • Define APIs / method signatures
  • Apply patterns where they earn their keep
  • Call out concurrency, edge cases, extensions

Concurrency gotchas

  • Always while in condition-variable waits
  • volatile on DCL singleton fields
  • Hold locks briefly; never across I/O
  • Prefer java.util.concurrent primitives over hand-rolled
  • Copy-on-write for read-heavy observer lists

Code smell → pattern

  • if (instanceof) chains → polymorphism / Strategy
  • Flag parameters → split method / State
  • Long parameter list → Builder / Parameter Object
  • Shotgun surgery on switch → Open/Closed refactor
  • God class → SRP split

Money & time

  • Store money as integer minor units (cents)
  • Inject a Clock — never call new Date() inside domain code
  • Time zones: persist in UTC; render in local
  • Amounts: use BigDecimal for tax/FX with explicit scale

Immutability checklist

  • All fields final
  • No setters
  • Defensive copies of incoming collections
  • Return copies or unmodifiable views
  • Prefer record / frozen dataclass

API naming heuristics

  • Verbs for methods, nouns for types
  • Boolean getters: isEmpty(), hasChildren()
  • Factory methods: of(), from(), parse()
  • Explicit over abbreviated
  • No primitive obsession — wrap IDs in types

Top testing tips

  • One behavior per test; one assertion cluster
  • Arrange / Act / Assert layout
  • Prefer fakes over mocks for collaborators you own
  • Seed random/test data for determinism
  • Run flakies 100x in CI before calling them "intermittent"
Rules (enforced at Wave-4 checksum sweep): - Zero external dependencies. - IIFE-wrapped, single global: window.VizLib. - Respects prefers-reduced-motion. - ARIA-live captions on every viz. - Lazy-init via IntersectionObserver. - No build step; copy-paste as-is. - NO innerHTML usage (XSS-safe by construction). Declarative spec shape lives in a ` Place AFTER the existing per-page sidebar/search script, BEFORE . Responsibilities: - #reading-progress width tracker - Keyboard shortcuts: j/k/t/? and '/' focus TOC search - '.copy-btn' injection on every
     - Theme toggle (html.theme-light) with localStorage
     - #kbd-help open/close
     - #sub-toc build from current page's 

inside visible module - Respects focus in inputs (no shortcut hijack) - No innerHTML anywhere ============================================================================ */ (function () { 'use strict'; if (window.__uxScriptLoaded) return; window.__uxScriptLoaded = true; // ---------- reading progress ---------- const progress = document.getElementById('reading-progress'); if (progress) { let ticking = false; function update() { const doc = document.documentElement; const scrolled = doc.scrollTop || document.body.scrollTop; const height = doc.scrollHeight - doc.clientHeight; const pct = height > 0 ? Math.min(100, (scrolled / height) * 100) : 0; progress.style.setProperty('--progress', pct + '%'); ticking = false; } window.addEventListener('scroll', () => { if (!ticking) { requestAnimationFrame(update); ticking = true; } }, { passive: true }); update(); } // ---------- copy buttons on
 ----------
  document.querySelectorAll('pre').forEach(pre => {
    if (pre.querySelector('.copy-btn')) return;
    const btn = document.createElement('button');
    btn.type = 'button';
    btn.className = 'copy-btn';
    btn.textContent = 'Copy';
    btn.setAttribute('aria-label', 'Copy code to clipboard');
    btn.addEventListener('click', async () => {
      const code = pre.querySelector('code') || pre;
      const text = code.textContent || '';
      try {
        await navigator.clipboard.writeText(text);
        btn.textContent = 'Copied';
        btn.dataset.copied = '1';
        setTimeout(() => { btn.textContent = 'Copy'; delete btn.dataset.copied; }, 1400);
      } catch (err) {
        btn.textContent = 'Fail';
        setTimeout(() => { btn.textContent = 'Copy'; }, 1200);
      }
    });
    pre.appendChild(btn);
  });

  // ---------- theme toggle ----------
  const LS_KEY = 'notebook-theme';
  const saved = (() => { try { return localStorage.getItem(LS_KEY); } catch (e) { return null; } })();
  if (saved === 'light') document.documentElement.classList.add('theme-light');
  function toggleTheme() {
    document.documentElement.classList.toggle('theme-light');
    const light = document.documentElement.classList.contains('theme-light');
    try { localStorage.setItem(LS_KEY, light ? 'light' : 'dark'); } catch (e) {}
  }

  // ---------- keyboard help dialog ----------
  const helpDialog = document.getElementById('kbd-help');
  function openHelp() { if (helpDialog) helpDialog.dataset.open = '1'; }
  function closeHelp() { if (helpDialog) delete helpDialog.dataset.open; }
  if (helpDialog) {
    helpDialog.addEventListener('click', (e) => { if (e.target === helpDialog) closeHelp(); });
    const close = helpDialog.querySelector('.kbd-close');
    if (close) close.addEventListener('click', closeHelp);
  }

  // ---------- global shortcuts ----------
  function inEditable(el) {
    if (!el) return false;
    const tag = (el.tagName || '').toLowerCase();
    return tag === 'input' || tag === 'textarea' || tag === 'select' || el.isContentEditable;
  }
  function modules() {
    return Array.from(document.querySelectorAll('.module'));
  }
  function visibleModuleIndex() {
    const mods = modules();
    const mid = window.innerHeight / 3;
    for (let i = 0; i < mods.length; i++) {
      const r = mods[i].getBoundingClientRect();
      if (r.top <= mid && r.bottom > mid) return i;
      if (r.top > mid) return Math.max(0, i - 1);
    }
    return mods.length - 1;
  }
  function scrollToModule(i) {
    const mods = modules();
    if (!mods.length) return;
    const clamped = Math.max(0, Math.min(mods.length - 1, i));
    mods[clamped].scrollIntoView({ behavior: 'smooth', block: 'start' });
  }
  document.addEventListener('keydown', (e) => {
    if (e.ctrlKey || e.metaKey || e.altKey) return;
    if (inEditable(e.target)) {
      if (e.key === 'Escape' && e.target.id === 'toc-search') e.target.blur();
      return;
    }
    if (e.key === '?') { e.preventDefault(); openHelp(); }
    else if (e.key === 'Escape') closeHelp();
    else if (e.key === 't' || e.key === 'T') { e.preventDefault(); toggleTheme(); }
    else if (e.key === 'j' || e.key === 'J') { e.preventDefault(); scrollToModule(visibleModuleIndex() + 1); }
    else if (e.key === 'k' || e.key === 'K') { e.preventDefault(); scrollToModule(visibleModuleIndex() - 1); }
    else if (e.key === '/') {
      const s = document.getElementById('toc-search');
      if (s) { e.preventDefault(); s.focus(); }
    }
  });

  // ---------- sub-TOC (right rail, wide only) ----------
  const subToc = document.getElementById('sub-toc');
  if (subToc) {
    function rebuild() {
      const mods = modules();
      const idx = visibleModuleIndex();
      if (idx < 0 || !mods[idx]) return;
      while (subToc.firstChild) subToc.removeChild(subToc.firstChild);
      const h = document.createElement('h5');
      h.textContent = 'On this module';
      subToc.appendChild(h);
      mods[idx].querySelectorAll('h3').forEach((h3, i) => {
        if (!h3.id) h3.id = (mods[idx].id || ('m' + idx)) + '-h3-' + i;
        const a = document.createElement('a');
        a.href = '#' + h3.id;
        a.textContent = (h3.textContent || '').trim();
        subToc.appendChild(a);
      });
    }
    let rTick = false;
    window.addEventListener('scroll', () => {
      if (!rTick) { requestAnimationFrame(() => { rebuild(); rTick = false; }); rTick = true; }
    }, { passive: true });
    rebuild();
  }
})();