Siddhant Deval
Siddhant Deval
system design22 min read

The Complete LLD Interview Playbook: TypeScript Systems Design in 60 Minutes

A field-tested 60-minute playbook for the LLD interview: how to translate any prompt into a production-grade TypeScript design, communicate architectural thinking clearly, and avoid the most common failure modes that trip up senior engineers under time pressure.

Complete LLD Interview Playbook: TypeScript Systems Design in 60 Minutes

In TypeScript 5+, OOP is an architectural contract, not an inheritance tree: enforce domain invariants at compile time, encapsulate mutation strictly within aggregates, and invert dependencies so high-level business policy never couples to execution details. This final article synthesizes all eleven preceding parts into a single, field-tested playbook for the LLD interview: how to translate any prompt into a production-grade TypeScript design within 60 minutes, under pressure, while communicating architectural thinking clearly.


1. The Anti-Pattern Graveyard: The Feature-Dump Interview

Here is the most common failure mode in an LLD interview — not lack of knowledge, but lack of framing:

Interviewer: "Design a payment processing system."

Candidate: "OK so I'll have a PaymentController, and it will call a PaymentService,
            and the service will have a PaymentGateway, and we'll use Stripe, and
            we need a database for transactions, and there should be error handling,
            and maybe a queue for async processing, and logging, and..."

[20 minutes later: no diagram, no typed interfaces, no entity relationships defined]

The interviewer is not evaluating how many features the candidate can enumerate. They are evaluating:

  1. Domain clarity — can the candidate identify the core entities and their invariants?
  2. Interface thinking — does the candidate invert dependencies naturally?
  3. Trade-off awareness — does the candidate know when a pattern is overkill?

The playbook below is the framework that produces the right output in the right sequence.


2. The 60-Minute Time Allocation

├── 0–5 min   → Requirement clarification (ask; don't code yet)
├── 5–15 min  → Entity modeling (class diagram, relationships, aggregates)
├── 15–20 min → Critical sequence diagram (happy path + one failure branch)
├── 20–45 min → Implement core domain in TypeScript
│               ├── Domain types (branded primitives, discriminated unions)
│               ├── Aggregate root with private state
│               ├── Interfaces + DIP (ports in domain, adapters in infra)
│               └── Key behavioral pattern (Strategy, Observer, or State Machine)
└── 45–60 min → Probe defense
                ├── "How would you add [new feature]?"
                ├── "What if [failure scenario] occurs?"
                └── "How would you test [component]?"

3. The 5-Question Clarification Checklist

In the first 5 minutes, ask exactly these questions. Do not start coding until you have answers:

TYPESCRIPT
// Mental checklist — ask these out loud:

const CLARIFICATION_QUESTIONS = [
  // 1. Primary entities
  "What are the core domain entities? (e.g., Order, Payment, Account, Customer?)",

  // 2. Operations
  "What are the primary operations? (Create, Update, Cancel, Refund, Query?)",

  // 3. Consistency requirement
  "Do we need strong consistency (no double-spend) or eventual consistency is acceptable?",

  // 4. Scale
  "Rough scale: how many concurrent users / transactions per second?",

  // 5. Extensibility
  "What is most likely to change in the future? (New payment providers? New order states?)",
] as const;

The answers determine everything: which patterns to apply (Strategy if "payment providers may change"), whether to use an Event Bus (eventual consistency) or a State Machine (strict transitions), and what to mention in the probe defense.


4. Pattern Selection Cheat Sheet

LLD Requirement Pattern Implementation Signal
Object construction with many optional fields Builder + Phantom Types this: Builder<Set,Set,Set> on .build()
Runtime type selection (gateway, provider) Factory + Strategy Registry satisfies Record<K, IStrategy>
Multiple output types from one input Discriminated Union type Result = | {ok: true; data: T} | {ok: false; error: string}
Object state with illegal combinations Discriminated Union FSM Each state is a structurally distinct variant
Notifications without coupling sender to receivers Observer / Event Bus Generic EventBus<T extends {kind:string}>
Reversible financial operations Command + History Stack interface ICommand { execute(); undo() }
Validation pipeline (ordered, independent) Chain of Responsibility setNext() + abstract passToNext()
Vendor SDK isolation Adapter (ACL) class VendorAdapter implements IDomainInterface
Simplified orchestration subsystem Facade One method coordinates N internal services
Recursive fee/pricing structures Composite Leaf + Composite implement same interface
Multiple class capabilities without deep inheritance Mixin Functions Higher-order function Timestampable<T>(Base: T)
Aggregate with strict lifecycle ownership Composition + #private Root guards all mutation; returns ReadonlyArray

5. Domain Entity Recognition Heuristic

When the interviewer describes the domain, run the "noun filter":

1. Extract all NOUNS from the problem statement.
   "Process orders from customers, each order has line items, 
   payments go through a gateway, refunds are tracked in a ledger."
   → Order, Customer, LineItem, Payment, Gateway, Refund, Ledger

2. Classify each noun:
   Entity (has identity, mutates)    → Order, Customer, Payment, Ledger
   Value Object (pure data, immutable) → Money, Address, DateRange
   Aggregate Root (guards cluster)  → OrderAggregate, LedgerEntry
   Service (stateless operations)   → PaymentService, RefundService
   Port (interface owned by domain) → IPaymentGateway, ILedgerRepository

3. Map relationships:
   Order *-- LineItem      (composition — lineitems die with order)
   Order --> Customer      (association — customer exists independently)
   Order --> IPaymentGateway (DIP — domain owns the interface)

6. Full 60-Minute Example: "Design a Payment Ledger System"

Minutes 0–5: Clarification

Requirements established:
- Entities: Account, LedgerEntry, Transaction
- Operations: Debit, Credit, Transfer, QueryBalance, ExportStatement
- Consistency: Strong (double-entry bookkeeping — every debit must have a matching credit)
- Scale: 1,000 concurrent transactions per second
- Change vector: New regulatory reporting formats; new currencies

Minutes 5–15: Class Diagram

Minutes 20–45: Core TypeScript Implementation

TYPESCRIPT
// ── domain/types.ts ──────────────────────────────────────────
declare const __brand: unique symbol;
type Brand<T, B extends string> = T & { readonly [__brand]: B };

type AccountId = Brand<string, 'AccountId'>;
type EntryId   = Brand<string, 'EntryId'>;

function asAccountId(raw: string): AccountId {
  if (!/^acc_[a-z0-9]{8,}$/.test(raw)) throw new Error(`Invalid AccountId: ${raw}`);
  return raw as AccountId;
}

// ── domain/Money.ts ──────────────────────────────────────────
class Money {
  constructor(readonly cents: number, readonly currency: 'USD' | 'EUR' | 'GBP') {
    if (!Number.isInteger(cents) || cents < 0) throw new RangeError('Invalid Money');
    Object.freeze(this);
  }
  add(other: Money): Money {
    if (this.currency !== other.currency) throw new Error('Currency mismatch');
    return new Money(this.cents + other.cents, this.currency);
  }
  subtract(other: Money): Money {
    if (this.cents < other.cents) throw new RangeError('Insufficient funds');
    return new Money(this.cents - other.cents, this.currency);
  }
}

// ── domain/AccountAggregate.ts ───────────────────────────────
class AccountAggregate {
  #balance: Money;
  readonly #entries: LedgerEntry[] = [];
  readonly accountId: AccountId;

  constructor(accountId: AccountId, initialBalance: Money) {
    this.accountId = accountId;
    this.#balance = initialBalance;
  }

  debit(amount: Money, reason: string): void {
    this.#balance = this.#balance.subtract(amount); // Throws if insufficient
    this.#entries.push(new LedgerEntry('DEBIT', amount, reason));
  }

  credit(amount: Money, reason: string): void {
    this.#balance = this.#balance.add(amount);
    this.#entries.push(new LedgerEntry('CREDIT', amount, reason));
  }

  get balance(): Money { return this.#balance; }
  get entries(): ReadonlyArray<LedgerEntry> { return this.#entries; }
}

type EntryType = 'DEBIT' | 'CREDIT';
class LedgerEntry {
  readonly entryId: EntryId = `ent_${Date.now()}_${Math.random().toString(36).slice(2)}` as EntryId;
  readonly occurredAt = new Date();
  constructor(
    readonly type: EntryType,
    readonly amount: Money,
    readonly reason: string,
  ) {}
}

// ── domain/ports/ILedgerRepository.ts ───────────────────────
interface ILedgerRepository {
  findById(id: AccountId): Promise<AccountAggregate | null>;
  save(account: AccountAggregate): Promise<void>;
}

// ── domain/TransferService.ts ────────────────────────────────
class TransferService {
  constructor(private readonly repo: ILedgerRepository) {}

  async transfer(fromId: AccountId, toId: AccountId, amount: Money): Promise<void> {
    const from = await this.repo.findById(fromId);
    const to   = await this.repo.findById(toId);

    if (!from) throw new Error(`Account not found: ${fromId}`);
    if (!to)   throw new Error(`Account not found: ${toId}`);

    from.debit(amount, `Transfer to ${toId}`);
    to.credit(amount, `Transfer from ${fromId}`);

    await Promise.all([this.repo.save(from), this.repo.save(to)]);
  }
}

Minutes 45–60: Probe Defense Answers

"How would you add new currencies?"

Money validates currency at construction. Extend the union type 'USD' | 'EUR' | 'GBP' | 'JPY'. The satisfies guard in CurrencyNormalizationMiddleware will error if the new currency is not handled. Zero AccountAggregate changes needed.

"What if the database write fails mid-transfer?"

Wrap both save() calls in a database transaction (unit-of-work pattern). The TransferService accepts an IUnitOfWork collaborator. The debit/credit mutations are already domain-level — the repository's transaction boundary is the infrastructure concern.

"How would you test this without a real database?"

Constructor injection with ILedgerRepository means: new TransferService(new InMemoryLedgerRepository()). The entire transfer logic is testable without any I/O.


7. The Senior Signal Checklist

These are the specific TypeScript 5+ patterns that signal senior-level architectural thinking. Use at least 4 in every LLD answer:

Signal Demonstrates
#private fields on aggregate root V8-level encapsulation, not just TypeScript modifiers
ReadonlyArray projections Aggregate mutation never leaks through getters
Branded primitives (AccountId, Money) Domain modeling, not just string/number passing
interface in domain, implementation in infra DIP applied correctly — not just "using interfaces"
satisfies Record<K, I> strategy registry OCP + ISP + compile-time exhaustiveness together
Discriminated union FSM State management without boolean flag explosion
Constructor injection throughout Testability without IoC framework
Symbol.dispose / try...finally on cleanup Lifecycle ownership awareness

Summary of the Series

This series built a complete enterprise TypeScript LLD foundation in 12 progressive parts:

Part Topic Core Skill
1 Class Internals & Encapsulation #private, prototype vs arrow, initialization order
2 Structural Typing & Branding Brand pattern, discriminated unions, satisfies
3 Contracts & Abstraction interface vs abstract class, mixins, noImplicitOverride
4 Object Lifecycles & Aggregates DDD aggregates, using, memory management
5 UML & Visual Architecture Mermaid class + sequence diagrams, interview sketching
6 SOLID (S, O, L) SRP decomposition, OCP strategy registry, LSP variance
7 SOLID (I, D) ISP role interfaces, DIP composition root, Stage 3 decorators
8 Creational Patterns Factory, Abstract Factory, Builder with phantom types
9 Structural Patterns Adapter ACL, Proxy, Facade, Composite fee tree
10 Behavioral: Strategy, Observer, Command Type-safe event bus, undo/redo ledger
11 Behavioral: FSM, Chain, Mediator Discriminated union states, middleware pipeline
12 LLD Interview Playbook 60-minute framework, pattern cheat sheet, senior signals
Research & Synthesis Note

This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.

#TypeScript#LLD Interview#Machine Coding#System Design#OOP#Interview Preparation
Siddhant Deval

Written by Siddhant Deval

Senior Full-Stack Engineer building high-scale architectures, browser performance engineering systems, and SaaS platforms.