Design Patterns Interview Mastery Guide

This guide covers the 23 Gang of Four (GoF) design patterns plus modern JavaScript/TypeScript patterns. Each pattern includes syntax, descriptions, real-world code examples, comparisons, and interview guidance. The patterns are grouped into Creational, Structural, and Behavioral categories.

PrerequisitesDescription
Object-Oriented ProgrammingClasses, interfaces, inheritance, polymorphism
JavaScript / TypeScriptES6+ syntax, modules, Promises, generics
SOLID PrinciplesSingle Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion

Table of contents

  1. Study strategy — Preparation timeline, SOLID principles
  2. Foundational Patterns (Q1-10) — Singleton, Factory, Builder, Prototype, Adapter, Decorator, Proxy, Facade, Composite, Observer
  3. See also — Related guides

Study strategy

LevelFocus areasStudy timeKey takeaway
Junior (0-2 yrs)Q1-Q10, Q16-Q204 weeksUnderstand Singleton, Factory, Observer, Module
Mid-Level (2-5 yrs)All Q1-Q306 weeksKnow when to apply each pattern and trade-offs
Senior (5+ yrs)All + Anti-patterns3 weeks reviewPattern combinations, architectural decisions

Preparation timeline

WeekTopics
1Creational Patterns (Singleton, Factory, Builder, Prototype)
2Structural Patterns (Decorator, Adapter, Proxy, Facade)
3Behavioral Patterns (Observer, Strategy, Command, State)
4Modern Patterns (Module, Mixin, Dependency Injection)
5-6Real implementations + Mock interviews

SOLID principles foundation

PrincipleMeaning
Single ResponsibilityOne class, one reason to change
Open/ClosedOpen for extension, closed for modification
Liskov SubstitutionSubtypes must be substitutable for base types
Interface SegregationMany specific interfaces over one general
Dependency InversionDepend on abstractions, not concretions

Foundational Patterns (Q1-10)

Q1: Singleton Pattern

The Singleton pattern ensures a class has only one instance and provides a global access point to that instance. It is commonly used for database connections, configuration managers, logging services, and other shared resources.

Syntax

ElementDescription
Private constructorPrevents direct instantiation with new
Static instance fieldHolds the single instance
Static getInstance() methodReturns or creates the instance
Lazy initializationInstance created on first access, not at load time

Description

Singleton restricts instantiation of a class to a single object. All subsequent calls to the constructor or getInstance() return the same reference. This is useful when exactly one object is needed to coordinate actions across the system.

Note: In JavaScript, the module system itself is a natural singleton — every import of a module returns the same cached reference.

Warning: Singletons can make unit testing difficult because they carry global state and are hard to mock. Prefer dependency injection for testable code.

Examples

Classic Singleton (JavaScript):

class Database {
  constructor() {
    if (Database.instance) {
      return Database.instance;
    }
    this.connection = null;
    this.config = {};
    Database.instance = this;
  }

  connect(connectionString) {
    if (!this.connection) {
      this.connection = `Connected to ${connectionString}`;
    }
    return this.connection;
  }

  query(sql) {
    if (!this.connection) {
      throw new Error('Not connected');
    }
    return `Executing: ${sql}`;
  }
}

const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // ✅ true — same instance

Module pattern Singleton (JavaScript):

const ConfigManager = (() => {
  let instance;

  function createInstance() {
    return {
      config: {},
      set(key, value) { this.config[key] = value; },
      get(key) { return this.config[key]; },
      getAll() { return { ...this.config }; }
    };
  }

  return {
    getInstance() {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    }
  };
})();

const config1 = ConfigManager.getInstance();
const config2 = ConfigManager.getInstance();
config1.set('apiUrl', 'https://api.example.com');
console.log(config2.get('apiUrl')); // ✅ 'https://api.example.com'

TypeScript Singleton (type-safe):

class Logger {
  private static instance: Logger;
  private logs: string[] = [];

  private constructor() {} // Private prevents direct instantiation

  public static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }

  public log(message: string): void {
    const timestamp = new Date().toISOString();
    this.logs.push(`[${timestamp}] ${message}`);
  }

  public getLogs(): string[] {
    return [...this.logs];
  }

  public clearLogs(): void {
    this.logs = [];
  }
}

const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
console.log(logger1 === logger2); // ✅ true

Lazy initialization with cache (TypeScript):

class CacheManager {
  private static instance: CacheManager;
  private cache: Map<string, any>;

  private constructor() {
    this.cache = new Map();
  }

  public static getInstance(): CacheManager {
    if (!CacheManager.instance) {
      CacheManager.instance = new CacheManager();
    }
    return CacheManager.instance;
  }

  public set(key: string, value: any, ttl?: number): void {
    this.cache.set(key, {
      value,
      expiry: ttl ? Date.now() + ttl : null
    });
  }

  public get(key: string): any {
    const item = this.cache.get(key);
    if (!item) return null;
    if (item.expiry && Date.now() > item.expiry) {
      this.cache.delete(key);
      return null;
    }
    return item.value;
  }

  public clear(): void {
    this.cache.clear();
  }
}

Comparison

Use caseSuitableReason
Database connectionsYesExpensive to create, should be shared
Configuration managersYesSingle source of truth
Logging servicesYesCentralized log management
Thread poolsYesLimited resource management
Multiple configs neededNoEach consumer needs its own instance
Testing with different setupsNoGlobal state makes mocking hard

Interview guidance

How to explain it:

"Singleton ensures a class has only one instance and provides a global access point. It is useful for database connections, configuration managers, and logging services — resources that should be shared across the application."

Common follow-ups:

QuestionKey points
What are drawbacks of Singleton?Hard to test/mock, violates Single Responsibility (manages own lifecycle + business logic), creates hidden dependencies via global state, issues in concurrent environments without synchronization
How do you handle Singleton in a multi-threaded environment?Double-checked locking, lazy holder idiom, or language-specific thread-safe initialization (Java enum singleton, C++ Meyer's Singleton). JavaScript is single-threaded but async initialization still needs care
Singleton vs module-level instance?In JavaScript, module exports are cached and act as natural singletons. Prefer module-scoped instances over class-based singletons when possible

Difficulty: Beginner


Q2: Factory Pattern

The Factory pattern provides an interface for creating objects without specifying the exact class. It delegates instantiation logic to a factory method or class. There are three main variations: Simple Factory, Factory Method, and Abstract Factory.

Syntax

VariationMechanismCreates
Simple FactoryStatic method with switch/mapSingle product type
Factory MethodSubclass overrides creation methodOne product per factory subclass
Abstract FactoryFactory interface with multiple methodsFamily of related products

Description

Factory patterns centralize object creation logic so that client code depends on abstractions rather than concrete classes. This makes it easy to add new types without modifying existing code (Open/Closed principle).

  • Simple Factory uses a single class with a method that returns different types based on input.
  • Factory Method defines an interface for creation but lets subclasses decide which class to instantiate.
  • Abstract Factory produces families of related objects without specifying their concrete classes.

Examples

Simple Factory:

class Car {
  constructor(options) {
    this.doors = options.doors || 4;
    this.state = options.state || 'new';
    this.color = options.color || 'silver';
  }
}

class Truck {
  constructor(options) {
    this.doors = options.doors || 2;
    this.state = options.state || 'used';
    this.wheelSize = options.wheelSize || 'large';
  }
}

class VehicleFactory {
  createVehicle(type, options) {
    switch (type) {
      case 'car': return new Car(options);
      case 'truck': return new Truck(options);
      default: throw new Error(`Unknown type: ${type}`);
    }
  }
}

const factory = new VehicleFactory();
const car = factory.createVehicle('car', { color: 'blue' });
const truck = factory.createVehicle('truck', { wheelSize: 'medium' });

Factory Method (TypeScript):

interface Button {
  render(): void;
  onClick(callback: () => void): void;
}

abstract class Dialog {
  abstract createButton(): Button;

  render(): void {
    const button = this.createButton();
    button.onClick(() => console.log('Button clicked!'));
    button.render();
  }
}

class WindowsButton implements Button {
  render(): void { console.log('Rendering Windows button'); }
  onClick(callback: () => void): void { callback(); }
}

class WebButton implements Button {
  render(): void { console.log('Rendering HTML button'); }
  onClick(callback: () => void): void { callback(); }
}

class WindowsDialog extends Dialog {
  createButton(): Button { return new WindowsButton(); }
}

class WebDialog extends Dialog {
  createButton(): Button { return new WebButton(); }
}

Abstract Factory (TypeScript):

interface GUIFactory {
  createButton(): Button;
  createCheckbox(): Checkbox;
}

interface Checkbox {
  render(): void;
}

class WindowsFactory implements GUIFactory {
  createButton(): Button { return new WindowsButton(); }
  createCheckbox(): Checkbox { return new WindowsCheckbox(); }
}

class MacFactory implements GUIFactory {
  createButton(): Button { return new MacButton(); }
  createCheckbox(): Checkbox { return new MacCheckbox(); }
}

// Client depends only on interfaces
class Application {
  private button: Button;
  private checkbox: Checkbox;

  constructor(factory: GUIFactory) {
    this.button = factory.createButton();
    this.checkbox = factory.createCheckbox();
  }

  render(): void {
    this.button.render();
    this.checkbox.render();
  }
}

Real-world example — Payment processing:

interface PaymentMethod {
  processPayment(amount: number): Promise<boolean>;
  refund(transactionId: string): Promise<boolean>;
}

class CreditCardPayment implements PaymentMethod {
  async processPayment(amount: number) {
    console.log(`Credit card: $${amount}`);
    return true;
  }
  async refund(transactionId: string) { return true; }
}

class PayPalPayment implements PaymentMethod {
  async processPayment(amount: number) {
    console.log(`PayPal: $${amount}`);
    return true;
  }
  async refund(transactionId: string) { return true; }
}

class PaymentFactory {
  static create(type: string): PaymentMethod {
    const methods: Record<string, () => PaymentMethod> = {
      credit_card: () => new CreditCardPayment(),
      paypal: () => new PayPalPayment(),
    };
    const factory = methods[type];
    if (!factory) throw new Error(`Unsupported: ${type}`);
    return factory();
  }
}

Comparison

AspectSimple FactoryFactory MethodAbstract Factory
ComplexityLowMediumHigh
FlexibilityLowMediumHigh
Use caseSingle product typeProduct hierarchyProduct families
GoF patternNoYesYes
Use caseSuitableReason
Complex object creationYesMultiple steps, validation needed
Type decided at runtimeYesDecouples creation from usage
Extending with new typesYesNo changes to existing code
Simple new MyClass()NoAdds unnecessary indirection

Interview guidance

How to explain it:

"Factory pattern provides an interface for creating objects without specifying their exact class. It delegates instantiation to factory methods or classes, making it easy to extend with new types without modifying existing code."

Common follow-ups:

QuestionKey points
Difference between Factory Method and Abstract Factory?Factory Method uses inheritance — subclasses decide which class to instantiate. Abstract Factory uses composition — one factory creates a family of related objects. Factory Method produces one product; Abstract Factory produces multiple related products
When is a factory overkill?When direct construction (new MyClass()) is simple and there is only one implementation. Factories add value when creation is complex or types vary

Difficulty: Beginner


Q3: Builder Pattern

The Builder pattern separates the construction of a complex object from its representation. It allows step-by-step creation and produces different representations using the same construction process. It is useful when objects have many optional parameters.

Syntax

ElementDescription
BuilderDefines step-by-step construction methods, returns this for chaining
Director (optional)Encapsulates common construction sequences
ProductThe complex object being built
build() methodFinalizes and returns the constructed object

Description

Builder eliminates the "telescoping constructor" anti-pattern where constructors take many parameters that are hard to read. Each setter method on the builder returns this, enabling fluent method chaining. An optional Director class can encapsulate predefined construction sequences.

Note: In JavaScript, the object parameter pattern (new User({ name, age, email })) can replace simple builders. Use Builder when construction involves validation, ordering, or multiple steps.

Examples

Classic Builder with method chaining (JavaScript):

class Pizza {
  constructor() {
    this.size = '';
    this.cheese = false;
    this.pepperoni = false;
    this.mushrooms = false;
    this.sauce = '';
  }

  describe() {
    const toppings = [];
    if (this.cheese) toppings.push('cheese');
    if (this.pepperoni) toppings.push('pepperoni');
    if (this.mushrooms) toppings.push('mushrooms');
    return `${this.size} pizza with ${this.sauce}, ${toppings.join(', ')}`;
  }
}

class PizzaBuilder {
  constructor() { this.pizza = new Pizza(); }
  setSize(size) { this.pizza.size = size; return this; }
  setSauce(sauce) { this.pizza.sauce = sauce; return this; }
  addCheese() { this.pizza.cheese = true; return this; }
  addPepperoni() { this.pizza.pepperoni = true; return this; }
  addMushrooms() { this.pizza.mushrooms = true; return this; }
  build() { return this.pizza; }
}

const pizza = new PizzaBuilder()
  .setSize('large')
  .setSauce('tomato')
  .addCheese()
  .addPepperoni()
  .build();

Builder with Director (TypeScript):

interface ComputerBuilder {
  setCPU(cpu: string): this;
  setRAM(ram: string): this;
  setStorage(storage: string): this;
  setGPU(gpu: string): this;
  setCooling(cooling: string): this;
  build(): Computer;
}

class Computer {
  constructor(
    public cpu: string,
    public ram: string,
    public storage: string,
    public gpu?: string,
    public cooling?: string
  ) {}
}

class GamingComputerBuilder implements ComputerBuilder {
  private cpu!: string;
  private ram!: string;
  private storage!: string;
  private gpu?: string;
  private cooling?: string;

  setCPU(cpu: string) { this.cpu = cpu; return this; }
  setRAM(ram: string) { this.ram = ram; return this; }
  setStorage(s: string) { this.storage = s; return this; }
  setGPU(gpu: string) { this.gpu = gpu; return this; }
  setCooling(c: string) { this.cooling = c; return this; }

  build(): Computer {
    return new Computer(
      this.cpu, this.ram, this.storage, this.gpu, this.cooling
    );
  }
}

// Director encapsulates standard configurations
class ComputerDirector {
  buildGamingPC(builder: ComputerBuilder): Computer {
    return builder
      .setCPU('Intel i9-13900K')
      .setRAM('32GB DDR5')
      .setStorage('2TB NVMe SSD')
      .setGPU('RTX 4090')
      .setCooling('Liquid Cooling')
      .build();
  }

  buildOfficePC(builder: ComputerBuilder): Computer {
    return builder
      .setCPU('Intel i5-13400')
      .setRAM('16GB DDR4')
      .setStorage('512GB SSD')
      .build();
  }
}

URL Builder (TypeScript):

class URLBuilder {
  private protocol = 'https';
  private domain = '';
  private path = '';
  private queryParams: Map<string, string> = new Map();
  private hash = '';

  setDomain(d: string) { this.domain = d; return this; }
  setPath(p: string) {
    this.path = p.startsWith('/') ? p : `/${p}`;
    return this;
  }
  addQueryParam(key: string, value: string) {
    this.queryParams.set(key, value);
    return this;
  }
  setHash(h: string) {
    this.hash = h.startsWith('#') ? h.slice(1) : h;
    return this;
  }

  build(): string {
    if (!this.domain) throw new Error('Domain is required');
    let url = `${this.protocol}://${this.domain}${this.path}`;
    if (this.queryParams.size > 0) {
      const params = Array.from(this.queryParams.entries())
        .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
        .join('&');
      url += `?${params}`;
    }
    if (this.hash) url += `#${this.hash}`;
    return url;
  }
}

const url = new URLBuilder()
  .setDomain('api.example.com')
  .setPath('/users/123')
  .addQueryParam('include', 'posts')
  .addQueryParam('limit', '10')
  .build();
// ✅ "https://api.example.com/users/123?include=posts&limit=10"

SQL Query Builder (TypeScript):

class QueryBuilder {
  private table = '';
  private selectFields: string[] = ['*'];
  private whereConditions: string[] = [];
  private orderByField = '';
  private orderDirection: 'ASC' | 'DESC' = 'ASC';
  private limitValue: number | null = null;

  from(table: string) { this.table = table; return this; }
  select(...fields: string[]) { this.selectFields = fields; return this; }
  where(condition: string) { this.whereConditions.push(condition); return this; }
  orderBy(field: string, dir: 'ASC' | 'DESC' = 'ASC') {
    this.orderByField = field;
    this.orderDirection = dir;
    return this;
  }
  limit(n: number) { this.limitValue = n; return this; }

  build(): string {
    if (!this.table) throw new Error('Table name is required');
    let q = `SELECT ${this.selectFields.join(', ')} FROM ${this.table}`;
    if (this.whereConditions.length > 0)
      q += ` WHERE ${this.whereConditions.join(' AND ')}`;
    if (this.orderByField)
      q += ` ORDER BY ${this.orderByField} ${this.orderDirection}`;
    if (this.limitValue !== null) q += ` LIMIT ${this.limitValue}`;
    return q;
  }
}

const query = new QueryBuilder()
  .select('id', 'name', 'email')
  .from('users')
  .where('age > 18')
  .where('status = "active"')
  .orderBy('created_at', 'DESC')
  .limit(10)
  .build();
// ✅ SELECT id, name, email FROM users WHERE age > 18
//    AND status = "active" ORDER BY created_at DESC LIMIT 10

Comparison

Use caseSuitableReason
Many constructor parameters (>4)YesAvoids telescoping constructor anti-pattern
Multi-step constructionYesEnforces order and validation
Immutable objectsYesBuild once, then freeze
Different representationsYesSame process, different products
Simple objects with few paramsNoUnnecessary complexity

Interview guidance

How to explain it:

"Builder separates the construction of a complex object from its representation. It allows step-by-step creation with method chaining and supports different representations using the same construction process."

Common follow-ups:

QuestionKey points
Builder vs constructor with optional parameters?Builders provide better readability via chaining, enforce construction order/validation, and support immutability. Optional parameters are simpler but become unreadable with many params
When to use a Director?When predefined construction sequences exist (e.g., "Gaming PC", "Office PC") that clients frequently request. Without a Director, clients build custom configurations directly

Difficulty: Intermediate


Q4: Prototype Pattern

The Prototype pattern creates new objects by cloning an existing prototype instance rather than constructing from scratch. JavaScript has built-in prototypal inheritance through Object.create(). This pattern is useful when object creation is expensive or complex.

Syntax

ElementDescription
PrototypeObject that serves as a template for cloning
clone() methodReturns a copy of the prototype
Prototype Registry (optional)Stores and retrieves named prototypes
Shallow vs Deep cloneShallow copies references; deep copies nested structures

Description

Instead of calling a constructor, new objects are produced by copying an existing object. This avoids expensive initialization when objects share most of their state. JavaScript natively supports this through its prototype chain and Object.create().

Warning: Shallow copies share references to nested objects. Modifying a nested property on the clone also modifies the original. Use deep cloning (structuredClone(), JSON.parse(JSON.stringify()), or libraries) for objects with nested state.

Examples

JavaScript prototypal inheritance (built-in):

const carPrototype = {
  init(make, model, year) {
    this.make = make;
    this.model = model;
    this.year = year;
  },
  getInfo() {
    return `${this.year} ${this.make} ${this.model}`;
  }
};

const car1 = Object.create(carPrototype);
car1.init('Toyota', 'Camry', 2023);

const car2 = Object.create(carPrototype);
car2.init('Honda', 'Accord', 2023);

console.log(car1.getInfo()); // ✅ "2023 Toyota Camry"

Explicit clone method:

class Circle {
  constructor(x, y, color, radius) {
    this.type = 'circle';
    this.x = x;
    this.y = y;
    this.color = color;
    this.radius = radius;
  }

  clone() {
    return new Circle(this.x, this.y, this.color, this.radius);
  }

  draw() {
    console.log(`Drawing ${this.color} circle at (${this.x}, ${this.y})`);
  }
}

const circle1 = new Circle(10, 20, 'red', 5);
const circle2 = circle1.clone();
circle2.x = 50;
console.log(circle1.x); // ✅ 10 — original unchanged
console.log(circle2.x); // ✅ 50

Shallow vs deep clone:

// ❌ Shallow copy — nested references shared
const obj1 = { nested: { value: 1 } };
const shallow = { ...obj1 };
shallow.nested.value = 2;
console.log(obj1.nested.value); // 2 — modified original!

// ✅ Deep copy — independent nested objects
const deep = JSON.parse(JSON.stringify(obj1));
deep.nested.value = 3;
console.log(obj1.nested.value); // 2 — unchanged

// ✅ Modern deep copy (no limitations with functions/dates)
const modern = structuredClone(obj1);

Note: JSON.parse(JSON.stringify()) cannot handle functions, Date objects, undefined, or circular references. Use structuredClone() or a library like lodash cloneDeep() for complex objects.

Prototype Registry (TypeScript):

interface Cloneable<T> {
  clone(): T;
}

class DocumentTemplate implements Cloneable<DocumentTemplate> {
  constructor(
    public title: string,
    public content: string,
    public styles: Record<string, any>,
    public metadata: Record<string, any>
  ) {}

  clone(): DocumentTemplate {
    return new DocumentTemplate(
      this.title,
      this.content,
      { ...this.styles },
      { ...this.metadata }
    );
  }
}

class TemplateManager {
  private templates = new Map<string, DocumentTemplate>();

  addTemplate(name: string, template: DocumentTemplate): void {
    this.templates.set(name, template);
  }

  createDocument(name: string, title: string, content: string) {
    const template = this.templates.get(name);
    if (!template) throw new Error(`Template "${name}" not found`);
    const doc = template.clone();
    doc.title = title;
    doc.content = content;
    return doc;
  }
}

Comparison

Use caseSuitableReason
Expensive object creationYesCloning avoids re-running costly initialization
Many similar object variationsYesClone and modify a few fields
Runtime object types unknownYesClone without knowing the concrete class
Cloning is as expensive as constructingNoNo benefit over new

Interview guidance

How to explain it:

"Prototype creates new objects by cloning an existing instance rather than constructing from scratch. JavaScript supports this natively through Object.create() and the prototype chain."

Common follow-ups:

QuestionKey points
Shallow copy vs deep copy?Shallow copies references to nested objects (mutations affect original). Deep copy creates independent nested objects. Use structuredClone() or lodash.cloneDeep() for deep copies
How does JavaScript prototypal inheritance relate?JavaScript uses prototypes natively for inheritance. The GoF Prototype pattern is about explicit cloning for object creation. In JS, Object.create() or the spread operator are idiomatic alternatives to explicit clone() methods

Difficulty: Beginner


Q5: Adapter Pattern

The Adapter pattern converts the interface of a class into another interface that clients expect. It allows classes with incompatible interfaces to work together by acting as a wrapper between them.

Syntax

ElementDescription
Target interfaceThe interface the client expects
AdapteeThe existing class with an incompatible interface
AdapterWraps the Adaptee and implements the Target interface
ClientWorks only with the Target interface

Description

Adapter is a structural pattern that bridges two incompatible interfaces. It wraps an existing class and translates calls from the expected interface into calls the wrapped class understands. This is commonly used when integrating legacy code, third-party libraries, or multiple implementations behind a unified interface.

Examples

Classic Adapter (TypeScript):

// Adaptee — old system with incompatible interface
class OldPrinter {
  printOldFormat(text: string): void {
    console.log(`OLD FORMAT: ${text.toUpperCase()}`);
  }
}

// Target interface
interface ModernPrinter {
  print(text: string): void;
  printColor(text: string, color: string): void;
}

// Adapter wraps Adaptee, implements Target
class PrinterAdapter implements ModernPrinter {
  constructor(private oldPrinter: OldPrinter) {}

  print(text: string): void {
    this.oldPrinter.printOldFormat(text);
  }

  printColor(text: string, color: string): void {
    this.oldPrinter.printOldFormat(`[${color}] ${text}`);
  }
}

function clientCode(printer: ModernPrinter): void {
  printer.print('Hello World');
}

const adapter = new PrinterAdapter(new OldPrinter());
clientCode(adapter); // ✅ Works transparently

API Adapter — payment gateways:

interface PaymentGateway {
  processPayment(amount: number, method: string): Promise<PaymentResult>;
}

interface PaymentResult {
  success: boolean;
  transactionId: string;
  amount: number;
}

// Stripe has its own interface
class StripeAPI {
  makePayment(amount: number, currency: string, cardToken: string) {
    return Promise.resolve({
      success: true,
      transaction_id: 'stripe_' + Math.random().toString(36).slice(2),
      amount_cents: amount * 100,
    });
  }
}

class StripeAdapter implements PaymentGateway {
  constructor(private stripe: StripeAPI) {}

  async processPayment(amount: number, method: string): Promise<PaymentResult> {
    const result = await this.stripe.makePayment(amount, 'USD', method);
    return {
      success: result.success,
      transactionId: result.transaction_id,  // Adapt field name
      amount: result.amount_cents / 100,     // Adapt unit
    };
  }
}

// PayPal has a different interface
class PayPalAPI {
  charge(dollars: number, account: string) {
    return Promise.resolve({
      status: 'completed',
      id: 'pp_' + Math.random().toString(36).slice(2),
      total: dollars,
    });
  }
}

class PayPalAdapter implements PaymentGateway {
  constructor(private paypal: PayPalAPI) {}

  async processPayment(amount: number, method: string): Promise<PaymentResult> {
    const result = await this.paypal.charge(amount, method);
    return {
      success: result.status === 'completed',
      transactionId: result.id,
      amount: result.total,
    };
  }
}

// Client code works with any PaymentGateway
async function checkout(gateway: PaymentGateway, amount: number) {
  const result = await gateway.processPayment(amount, 'token');
  console.log(`Payment ${result.success ? 'successful' : 'failed'}`);
}

Database Adapter:

interface Database {
  connect(): Promise<void>;
  query(sql: string): Promise<any[]>;
  disconnect(): Promise<void>;
}

class MySQLDatabase {
  async mysqlConnect(host: string, user: string) {
    console.log(`MySQL connected to ${host}`);
  }
  async mysqlQuery(query: string) { return []; }
  async mysqlClose() { console.log('MySQL disconnected'); }
}

class MySQLAdapter implements Database {
  constructor(private mysql: MySQLDatabase) {}
  async connect() { await this.mysql.mysqlConnect('localhost', 'root'); }
  async query(sql: string) { return this.mysql.mysqlQuery(sql); }
  async disconnect() { await this.mysql.mysqlClose(); }
}

class MongoDatabase {
  async mongoConnect(uri: string) {
    console.log(`MongoDB connected to ${uri}`);
  }
  async mongoFind(collection: string, filter: any) { return []; }
  async mongoDisconnect() { console.log('MongoDB disconnected'); }
}

class MongoAdapter implements Database {
  constructor(private mongo: MongoDatabase) {}
  async connect() {
    await this.mongo.mongoConnect('mongodb://localhost:27017');
  }
  async query(sql: string) {
    // Simplified: extract table name from SQL
    const collection = sql.match(/FROM (\w+)/)?.[1] || 'default';
    return this.mongo.mongoFind(collection, {});
  }
  async disconnect() { await this.mongo.mongoDisconnect(); }
}

// Swap databases without changing application code
class App {
  constructor(private db: Database) {}
  async run() {
    await this.db.connect();
    await this.db.query('SELECT * FROM users');
    await this.db.disconnect();
  }
}

Comparison

Use caseSuitableReason
Legacy code integrationYesMakes old code work with new systems
Third-party library wrappingYesStandardizes incompatible APIs
Multiple implementationsYesUnifies interfaces (payment gateways, databases)
Can modify the sourceNoRefactor the interface directly instead
PatternPurpose
AdapterConverts one interface to another (compatibility)
FacadeSimplifies a complex subsystem (simplification)
DecoratorAdds behavior to an object (enhancement)

Interview guidance

How to explain it:

"Adapter converts the interface of an existing class into another interface that clients expect. It acts as a wrapper that translates calls, allowing incompatible interfaces to work together."

Common follow-ups:

QuestionKey points
Adapter vs Facade?Adapter converts one interface to match another. Facade simplifies a complex subsystem with a new, simpler interface. Adapter works with existing interfaces; Facade defines a new one
Object Adapter vs Class Adapter?Object Adapter uses composition (wraps adaptee). Class Adapter uses multiple inheritance (extends adaptee). JavaScript/TypeScript only supports Object Adapter since they lack multiple inheritance

Difficulty: Beginner


Q6: Decorator Pattern

The Decorator pattern attaches additional responsibilities to an object dynamically. It provides a flexible alternative to subclassing for extending functionality. Decorators wrap the original object and add new behavior while maintaining the same interface.

Syntax

ElementDescription
Component interfaceDefines the common interface
Concrete ComponentThe base object being decorated
Base DecoratorWraps a Component and delegates to it
Concrete DecoratorsAdd specific behavior before/after delegation

Description

Decorators are stacked (composed) around a base object. Each decorator implements the same interface, so the client cannot tell whether it is working with the base object or a decorated version. This supports the Open/Closed principle — you can add behavior without modifying existing classes.

Warning: Decorator order matters. Stacking decorators in different orders can produce different results. Document expected ordering when it is significant.

Examples

Classic Decorator — coffee shop:

interface Coffee {
  cost(): number;
  description(): string;
}

class SimpleCoffee implements Coffee {
  cost() { return 2; }
  description() { return 'Simple coffee'; }
}

abstract class CoffeeDecorator implements Coffee {
  constructor(protected coffee: Coffee) {}
  abstract cost(): number;
  abstract description(): string;
}

class MilkDecorator extends CoffeeDecorator {
  cost() { return this.coffee.cost() + 0.5; }
  description() { return this.coffee.description() + ', milk'; }
}

class SugarDecorator extends CoffeeDecorator {
  cost() { return this.coffee.cost() + 0.2; }
  description() { return this.coffee.description() + ', sugar'; }
}

class WhippedCreamDecorator extends CoffeeDecorator {
  cost() { return this.coffee.cost() + 0.7; }
  description() { return this.coffee.description() + ', whipped cream'; }
}

// Stack decorators
let coffee: Coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
coffee = new WhippedCreamDecorator(coffee);
console.log(`${coffee.description()}: $${coffee.cost()}`);
// ✅ "Simple coffee, milk, sugar, whipped cream: $3.4"

Function composition (decorator-like middleware):

type Middleware = (next: Function) => Function;

const logger: Middleware = (next) => (...args: any[]) => {
  console.log('Logging:', args);
  return next(...args);
};

const timer: Middleware = (next) => (...args: any[]) => {
  const start = Date.now();
  const result = next(...args);
  console.log(`Execution time: ${Date.now() - start}ms`);
  return result;
};

const errorHandler: Middleware = (next) => (...args: any[]) => {
  try { return next(...args); }
  catch (error) { console.error('Error:', error); throw error; }
};

function compose(...middlewares: Middleware[]) {
  return (fn: Function) =>
    middlewares.reduceRight((acc, mw) => mw(acc), fn);
}

const add = (x: number, y: number) => x + y;
const decorated = compose(logger, timer, errorHandler)(add);
decorated(5, 3);

HTTP client with stacked decorators:

interface HttpClient {
  get(url: string): Promise<any>;
  post(url: string, data: any): Promise<any>;
}

class BasicHttpClient implements HttpClient {
  async get(url: string) { return (await fetch(url)).json(); }
  async post(url: string, data: any) {
    return (await fetch(url, {
      method: 'POST', body: JSON.stringify(data)
    })).json();
  }
}

class AuthHttpClient implements HttpClient {
  constructor(private client: HttpClient, private token: string) {}
  async get(url: string) {
    return this.client.get(`${url}?token=${this.token}`);
  }
  async post(url: string, data: any) {
    return this.client.post(url, { ...data, token: this.token });
  }
}

class LoggingHttpClient implements HttpClient {
  constructor(private client: HttpClient) {}
  async get(url: string) {
    console.log(`GET ${url}`);
    return this.client.get(url);
  }
  async post(url: string, data: any) {
    console.log(`POST ${url}`, data);
    return this.client.post(url, data);
  }
}

class CachingHttpClient implements HttpClient {
  private cache = new Map<string, any>();
  constructor(private client: HttpClient, private ttl = 5000) {}

  async get(url: string) {
    if (this.cache.has(url)) return this.cache.get(url);
    const result = await this.client.get(url);
    this.cache.set(url, result);
    setTimeout(() => this.cache.delete(url), this.ttl);
    return result;
  }
  async post(url: string, data: any) {
    return this.client.post(url, data); // No caching for POST
  }
}

// ✅ Stack decorators transparently
let client: HttpClient = new BasicHttpClient();
client = new AuthHttpClient(client, 'secret_token');
client = new LoggingHttpClient(client);
client = new CachingHttpClient(client, 10000);

React Higher-Order Components (decorator pattern):

function withLoading<P extends { data: any }>(
  Component: React.ComponentType<P>
): React.ComponentType<P & { isLoading: boolean }> {
  return (props) => {
    if (props.isLoading) return <div>Loading...</div>;
    return <Component {...props} />;
  };
}

function withErrorBoundary<P>(
  Component: React.ComponentType<P>
): React.ComponentType<P> {
  return class extends React.Component<P, { hasError: boolean }> {
    state = { hasError: false };
    componentDidCatch() { this.setState({ hasError: true }); }
    render() {
      if (this.state.hasError) return <div>Something went wrong</div>;
      return <Component {...this.props} />;
    }
  };
}

const EnhancedList = withErrorBoundary(withLoading(UserList));

Comparison

Use caseSuitableReason
Add responsibilities dynamicallyYesRuntime behavior addition
Avoid subclass explosionYesCombine features via stacking instead of inheritance
Open/Closed principleYesExtend without modifying
Simple inheritance sufficesNoDecorator adds unnecessary wrapping
PatternPurpose
DecoratorAdds new behavior (enhancement)
ProxyControls access to an object (access control)
AdapterConverts one interface to match another (compatibility)

Interview guidance

How to explain it:

"Decorator attaches additional responsibilities to an object dynamically by wrapping it. It provides a flexible alternative to subclassing — you can stack multiple decorators to compose complex behavior while keeping the same interface."

Common follow-ups:

QuestionKey points
Decorator vs Proxy?Decorator adds functionality (enhancement). Proxy controls access (access control, lazy loading). Both wrap objects but with different intent
TypeScript decorators vs GoF Decorator?TypeScript decorators are compile-time metaprogramming annotations (@LogClass). GoF Decorator wraps objects at runtime. Similar concept, different implementation level
Can decorator order matter?Yes. Stacking LoggingDecorator then CachingDecorator logs cache hits. Reversing the order logs only network requests. Document expected order

Difficulty: Intermediate


Q7: Proxy Pattern

The Proxy pattern provides a surrogate or placeholder for another object to control access to it. Common proxy types include Virtual (lazy loading), Protection (access control), Remote (network calls), and Smart (logging, reference counting).

Syntax

ElementDescription
Subject interfaceCommon interface for Real Subject and Proxy
Real SubjectThe actual object performing the work
ProxyControls access to the Real Subject, same interface
Proxy typePurposeExample
VirtualLazy loadingDefer expensive image loading
ProtectionAccess controlUser permission checks
RemoteNetwork accessRPC, web service calls
SmartAdditional logicLogging, caching, reference counting

Description

A Proxy holds a reference to the Real Subject and intercepts calls to it. The client interacts with the Proxy through the same interface as the Real Subject, unaware that a proxy is involved. JavaScript provides a built-in Proxy object for creating proxies at the language level.

Examples

Virtual Proxy — lazy loading:

interface Image {
  display(): void;
}

class RealImage implements Image {
  constructor(private filename: string) {
    this.loadFromDisk(); // Expensive operation
  }
  private loadFromDisk(): void {
    console.log(`Loading ${this.filename} from disk...`);
  }
  display(): void {
    console.log(`Displaying ${this.filename}`);
  }
}

class ImageProxy implements Image {
  private realImage: RealImage | null = null;
  constructor(private filename: string) {}

  display(): void {
    if (!this.realImage) {
      this.realImage = new RealImage(this.filename); // Load on first use
    }
    this.realImage.display();
  }
}

const image = new ImageProxy('photo.jpg');
// ✅ Image not loaded yet
image.display(); // Loads, then displays
image.display(); // Already loaded — just displays

Protection Proxy — access control:

interface BankAccount {
  deposit(amount: number): void;
  withdraw(amount: number): void;
  getBalance(): number;
}

class RealBankAccount implements BankAccount {
  private balance = 0;
  deposit(amount: number) {
    this.balance += amount;
    console.log(`Deposited $${amount}. Balance: $${this.balance}`);
  }
  withdraw(amount: number) {
    if (amount <= this.balance) {
      this.balance -= amount;
      console.log(`Withdrew $${amount}. Balance: $${this.balance}`);
    } else console.log('Insufficient funds');
  }
  getBalance() { return this.balance; }
}

class BankAccountProxy implements BankAccount {
  private account = new RealBankAccount();
  constructor(private permissions: string[]) {}

  private hasPermission(action: string): boolean {
    return this.permissions.includes(action);
  }
  deposit(amount: number) {
    if (!this.hasPermission('deposit'))
      return console.log('Access denied: deposit');
    this.account.deposit(amount);
  }
  withdraw(amount: number) {
    if (!this.hasPermission('withdraw'))
      return console.log('Access denied: withdraw');
    this.account.withdraw(amount);
  }
  getBalance() {
    if (!this.hasPermission('view_balance')) return -1;
    return this.account.getBalance();
  }
}

const admin = new BankAccountProxy(['deposit', 'withdraw', 'view_balance']);
admin.deposit(1000);  // ✅ Success

const guest = new BankAccountProxy(['view_balance']);
guest.deposit(100);   // ❌ Access denied

Caching Proxy:

interface DataService {
  fetchData(id: string): Promise<any>;
}

class RealDataService implements DataService {
  async fetchData(id: string) {
    console.log(`Fetching ${id} from database...`);
    await new Promise(r => setTimeout(r, 1000)); // Simulate delay
    return { id, data: `Data for ${id}` };
  }
}

class CachingProxy implements DataService {
  private cache = new Map<string, any>();
  private service = new RealDataService();

  async fetchData(id: string) {
    if (this.cache.has(id)) {
      console.log(`Cache hit for ${id}`);
      return this.cache.get(id);
    }
    const data = await this.service.fetchData(id);
    this.cache.set(id, data);
    return data;
  }
}

ES6 Proxy (built-in):

const person = { name: 'John', age: 30 };

const proxy = new Proxy(person, {
  get(target, prop) {
    console.log(`Getting ${String(prop)}`);
    return target[prop];
  },
  set(target, prop, value) {
    if (prop === 'age' && typeof value !== 'number') {
      throw new TypeError('Age must be a number');
    }
    target[prop] = value;
    return true;
  }
});

proxy.name;      // ✅ Logs "Getting name", returns "John"
proxy.age = 31;  // ✅ Valid
proxy.age = 'x'; // ❌ TypeError: Age must be a number

Rate-limiting Proxy:

class APIClient {
  async makeRequest(endpoint: string) {
    console.log(`Request to ${endpoint}`);
    return { data: 'Response' };
  }
}

class RateLimitingProxy {
  private client = new APIClient();
  private requests = 0;
  private resetTime: number;

  constructor(private maxRequests: number, private windowMs: number) {
    this.resetTime = Date.now() + windowMs;
  }

  async makeRequest(endpoint: string) {
    if (Date.now() > this.resetTime) {
      this.requests = 0;
      this.resetTime = Date.now() + this.windowMs;
    }
    if (this.requests >= this.maxRequests) {
      throw new Error('Rate limit exceeded');
    }
    this.requests++;
    return this.client.makeRequest(endpoint);
  }
}

Generic logging Proxy:

function createLoggingProxy<T extends object>(target: T, name: string): T {
  return new Proxy(target, {
    get(obj, prop: string) {
      const value = (obj as any)[prop];
      if (typeof value === 'function') {
        return function (...args: any[]) {
          console.log(`[${name}] ${prop}(`, args, ')');
          const result = value.apply(obj, args);
          console.log(`[${name}] ${prop} =>`, result);
          return result;
        };
      }
      return value;
    }
  });
}

const calc = { add: (a: number, b: number) => a + b };
const logged = createLoggingProxy(calc, 'Calc');
logged.add(2, 3); // ✅ Logs call and return value

Comparison

Use caseSuitableReason
Lazy loadingYesDefer expensive initialization
Access controlYesRestrict operations by permission
CachingYesStore and reuse expensive results
Logging / monitoringYesIntercept and record access
Direct access is sufficientNoProxy adds unnecessary indirection

Interview guidance

How to explain it:

"Proxy provides a surrogate object that controls access to another object. It uses the same interface as the real object, so the client is unaware of the proxy. Common uses include lazy loading, access control, caching, and logging."

Common follow-ups:

QuestionKey points
Virtual Proxy vs Lazy Initialization?Virtual Proxy is a design pattern that uses lazy initialization as an implementation technique. The Proxy provides the same interface as the real object, hiding the deferred loading from the client
Proxy vs Decorator?Proxy controls access to an object. Decorator adds behavior to an object. Both wrap objects but differ in intent
ES6 Proxy use cases?Validation, property access interception, reactive data binding (Vue.js), operator overloading, default property values

Difficulty: Intermediate


Q8: Facade Pattern

The Facade pattern provides a simplified, unified interface to a complex subsystem. It hides the complexity of multiple interacting classes behind a single, easy-to-use API.

Syntax

ElementDescription
FacadeSingle class with simple methods that orchestrate the subsystem
Subsystem classesComplex internal classes that do the real work
ClientInteracts only with the Facade

Description

Facade does not add new functionality — it simplifies access to existing functionality. The subsystem classes remain accessible directly if needed, but most clients only use the Facade. This promotes loose coupling between clients and subsystems.

Note: Facade is one of the most commonly used patterns in real-world applications. API clients, ORMs, and SDK wrappers are all facades.

Examples

Home theater system:

class DVDPlayer {
  on() { console.log('DVD Player on'); }
  play(movie: string) { console.log(`Playing "${movie}"`); }
  stop() { console.log('DVD stopped'); }
  off() { console.log('DVD Player off'); }
}

class Projector {
  on() { console.log('Projector on'); }
  wideScreenMode() { console.log('Wide screen mode'); }
  off() { console.log('Projector off'); }
}

class SoundSystem {
  on() { console.log('Sound system on'); }
  setVolume(level: number) { console.log(`Volume: ${level}`); }
  setSurroundSound() { console.log('Surround sound enabled'); }
  off() { console.log('Sound system off'); }
}

class Lights {
  dim(level: number) { console.log(`Lights dimmed to ${level}%`); }
  on() { console.log('Lights on'); }
}

// Facade — one method replaces many subsystem calls
class HomeTheaterFacade {
  constructor(
    private dvd: DVDPlayer,
    private projector: Projector,
    private sound: SoundSystem,
    private lights: Lights
  ) {}

  watchMovie(movie: string): void {
    this.lights.dim(10);
    this.projector.on();
    this.projector.wideScreenMode();
    this.sound.on();
    this.sound.setSurroundSound();
    this.sound.setVolume(15);
    this.dvd.on();
    this.dvd.play(movie);
  }

  endMovie(): void {
    this.dvd.stop();
    this.dvd.off();
    this.sound.off();
    this.projector.off();
    this.lights.on();
  }
}

// ✅ Client uses one simple call
const theater = new HomeTheaterFacade(
  new DVDPlayer(), new Projector(), new SoundSystem(), new Lights()
);
theater.watchMovie('Inception');
theater.endMovie();

Payment processing Facade:

class PaymentValidator {
  validateCard(cardNumber: string) { return cardNumber.length === 16; }
  validateAmount(amount: number) { return amount > 0 && amount < 10000; }
}

class FraudDetection {
  check(userId: string, amount: number) { return amount < 5000; }
}

class PaymentGateway {
  charge(amount: number, card: string) {
    return Promise.resolve('txn_' + Math.random().toString(36).slice(2));
  }
}

class NotificationService {
  sendEmail(userId: string, msg: string) {
    console.log(`Email to ${userId}: ${msg}`);
  }
}

class TransactionLogger {
  log(transaction: any) { console.log('Logged:', transaction); }
}

// Facade — one method orchestrates five subsystems
class PaymentFacade {
  private validator = new PaymentValidator();
  private fraud = new FraudDetection();
  private gateway = new PaymentGateway();
  private notify = new NotificationService();
  private logger = new TransactionLogger();

  async processPayment(userId: string, amount: number, card: string) {
    if (!this.validator.validateCard(card))
      return { success: false, error: 'Invalid card' };
    if (!this.validator.validateAmount(amount))
      return { success: false, error: 'Invalid amount' };
    if (!this.fraud.check(userId, amount))
      return { success: false, error: 'Flagged as fraud' };

    const txnId = await this.gateway.charge(amount, card);
    this.logger.log({ userId, amount, txnId });
    this.notify.sendEmail(userId, `Payment of $${amount} successful`);
    return { success: true, transactionId: txnId };
  }
}

// ✅ Client uses one call
const payment = new PaymentFacade();
await payment.processPayment('user123', 100, '1234567890123456');

Database Facade (simplified ORM):

class ConnectionPool {
  getConnection() { return {}; }
  releaseConnection(conn: any) {}
}

class QueryExecutor {
  async execute(query: string, params: any[]) { return []; }
}

class TransactionManager {
  async begin() {}
  async commit() {}
  async rollback() {}
}

// Facade hides connection pooling, transactions, mapping
class Database {
  private pool = new ConnectionPool();
  private executor = new QueryExecutor();
  private txn = new TransactionManager();

  async find<T>(table: string, id: number): Promise<T | null> {
    const conn = this.pool.getConnection();
    try {
      const rows = await this.executor.execute(
        `SELECT * FROM ${table} WHERE id = ?`, [id]
      );
      return (rows[0] as T) || null;
    } finally {
      this.pool.releaseConnection(conn);
    }
  }

  async save(table: string, data: any): Promise<void> {
    const conn = this.pool.getConnection();
    try {
      await this.txn.begin();
      // insert/update logic
      await this.txn.commit();
    } catch (error) {
      await this.txn.rollback();
      throw error;
    } finally {
      this.pool.releaseConnection(conn);
    }
  }
}

// ✅ Simple
const db = new Database();
const user = await db.find('users', 123);

Comparison

Use caseSuitableReason
Complex subsystem with many classesYesSimplifies client interaction
Multi-step common workflowYesEncapsulates repeated sequences
Third-party library wrappingYesProvides stable, simple interface
Already simple systemNoAdds unnecessary indirection
PatternPurpose
FacadeSimplifies a complex subsystem (many to one)
AdapterConverts one interface to another (compatibility)
MediatorCoordinates communication between objects

Interview guidance

How to explain it:

"Facade provides a simplified unified interface to a complex subsystem. It does not add new functionality — it makes existing functionality easier to use by hiding complexity behind a single API."

Common follow-ups:

QuestionKey points
Facade vs Adapter?Facade simplifies (many classes to one simple interface). Adapter converts (one incompatible interface to another). Facade creates a new simpler interface; Adapter makes existing interfaces work together
Does Facade violate Single Responsibility?It can, if the Facade does too much. A well-designed Facade delegates all work to subsystem classes and only orchestrates the workflow. Split large Facades into domain-specific ones

Difficulty: Beginner


Q9: Composite Pattern

The Composite pattern composes objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects (leaves) and compositions (composites) uniformly through a shared interface.

Syntax

ElementDescription
ComponentCommon interface for leaves and composites
LeafEnd object with no children
CompositeObject that contains children (leaves or other composites)
add() / remove()Methods on Composite to manage children

Description

Composite enables recursive composition. A Composite node delegates operations to its children, which may be leaves or other composites. This is the same pattern used in file systems (files and folders), UI component trees, and organizational hierarchies.

Note: The key insight is uniform treatment — the client calls the same method on both a leaf and a composite. The composite recursively aggregates results from its children.

Examples

File system:

interface FileSystemComponent {
  getName(): string;
  getSize(): number;
  print(indent?: string): void;
}

class File implements FileSystemComponent {
  constructor(private name: string, private size: number) {}
  getName() { return this.name; }
  getSize() { return this.size; }
  print(indent = '') {
    console.log(`${indent}${this.name} (${this.size} KB)`);
  }
}

class Folder implements FileSystemComponent {
  private children: FileSystemComponent[] = [];
  constructor(private name: string) {}

  add(component: FileSystemComponent) { this.children.push(component); }
  remove(component: FileSystemComponent) {
    const i = this.children.indexOf(component);
    if (i !== -1) this.children.splice(i, 1);
  }

  getName() { return this.name; }
  getSize() {
    // Recursively sums all children
    return this.children.reduce((sum, c) => sum + c.getSize(), 0);
  }
  print(indent = '') {
    console.log(`${indent}${this.name}/ (${this.getSize()} KB)`);
    this.children.forEach(c => c.print(indent + '  '));
  }
}

const root = new Folder('root');
const docs = new Folder('documents');
docs.add(new File('resume.pdf', 250));
docs.add(new File('cover-letter.doc', 150));
root.add(docs);
root.add(new File('system.log', 50));
root.print();
// ✅ root/ (450 KB)
//      documents/ (400 KB)
//        resume.pdf (250 KB)
//        cover-letter.doc (150 KB)
//      system.log (50 KB)

Organization structure:

interface Employee {
  getName(): string;
  getSalary(): number;
  print(indent?: string): void;
}

class Developer implements Employee {
  constructor(private name: string, private salary: number) {}
  getName() { return this.name; }
  getSalary() { return this.salary; }
  print(indent = '') {
    console.log(`${indent}${this.name} (Developer) - $${this.salary}`);
  }
}

class Manager implements Employee {
  private team: Employee[] = [];
  constructor(private name: string, private salary: number) {}

  addTeamMember(employee: Employee) { this.team.push(employee); }
  getName() { return this.name; }
  getSalary() {
    // Total includes own salary + all reports
    return this.salary + this.team.reduce((s, e) => s + e.getSalary(), 0);
  }
  print(indent = '') {
    console.log(`${indent}${this.name} (Manager) - $${this.salary}`);
    console.log(`${indent}  Team budget: $${this.getSalary()}`);
    this.team.forEach(m => m.print(indent + '  '));
  }
}

const cto = new Manager('Alice', 150000);
const lead = new Manager('Bob', 120000);
lead.addTeamMember(new Developer('Charlie', 90000));
lead.addTeamMember(new Developer('Diana', 85000));
cto.addTeamMember(lead);
cto.print();

UI component tree:

interface UIComponent {
  render(): string;
}

class Button implements UIComponent {
  constructor(private text: string) {}
  render() { return `<button>${this.text}</button>`; }
}

class Input implements UIComponent {
  constructor(private type: string, private placeholder: string) {}
  render() {
    return `<input type="${this.type}" placeholder="${this.placeholder}" />`;
  }
}

class Container implements UIComponent {
  private children: UIComponent[] = [];
  constructor(private tag = 'div') {}

  add(component: UIComponent) { this.children.push(component); }

  render(): string {
    const inner = this.children.map(c => c.render()).join('\n  ');
    return `<${this.tag}>\n  ${inner}\n</${this.tag}>`;
  }
}

const form = new Container('form');
form.add(new Input('text', 'Enter name'));
form.add(new Input('email', 'Enter email'));

const buttons = new Container('div');
buttons.add(new Button('Submit'));
buttons.add(new Button('Cancel'));
form.add(buttons);

console.log(form.render());

Menu system:

interface MenuComponent {
  execute(): void;
  print(indent?: string): void;
}

class MenuItem implements MenuComponent {
  constructor(private name: string, private action: () => void) {}
  execute() { this.action(); }
  print(indent = '') { console.log(`${indent}- ${this.name}`); }
}

class Menu implements MenuComponent {
  private items: MenuComponent[] = [];
  constructor(private name: string) {}

  add(item: MenuComponent) { this.items.push(item); }
  execute() { console.log(`Opening menu: ${this.name}`); }
  print(indent = '') {
    console.log(`${indent}${this.name}`);
    this.items.forEach(i => i.print(indent + '  '));
  }
}

const mainMenu = new Menu('Main');
const fileMenu = new Menu('File');
fileMenu.add(new MenuItem('New', () => console.log('New file')));
fileMenu.add(new MenuItem('Open', () => console.log('Open file')));
mainMenu.add(fileMenu);
mainMenu.print();

Comparison

Use caseSuitableReason
Part-whole hierarchiesYesTrees: file system, UI, org chart
Uniform treatment of itemsYesClient calls same method on leaf and composite
Recursive structuresYesComposites contain other composites
Fundamentally different objectsNoCannot share a meaningful interface

Interview guidance

How to explain it:

"Composite composes objects into tree structures to represent part-whole hierarchies. Clients treat individual objects and compositions uniformly through a shared interface."

Common follow-ups:

QuestionKey points
How to handle leaf-specific methods?Three approaches: (1) Add to component interface but throw in Composite (GoF approach). (2) Type check before calling (less type-safe). (3) Separate interfaces for Leaf and Composite (most type-safe but loses uniformity). It is a trade-off between type safety and uniform treatment
Real-world examples?File systems (File/Folder), React component tree, DOM nodes, organizational charts, menu systems, mathematical expression trees

Difficulty: Intermediate


Q10: Observer Pattern

The Observer pattern defines a one-to-many dependency between objects. When one object (the Subject) changes state, all its dependents (Observers) are notified and updated automatically. It implements a publish-subscribe mechanism.

Syntax

ElementDescription
Subject (Publisher)Maintains a list of observers, sends notifications
Observer (Subscriber)Defines an update() method called on state change
attach() / subscribe()Registers an observer
detach() / unsubscribe()Removes an observer
notify()Iterates observers and calls update()

Description

Observer decouples the subject from its dependents. The subject does not need to know the concrete classes of its observers — only that they implement the update() interface. This pattern is the foundation of event systems, data binding, and reactive programming.

Warning: Observers that are not detached can cause memory leaks because the subject holds references that prevent garbage collection. Always provide and use an unsubscribe mechanism. In React, use useEffect cleanup; in Angular, unsubscribe from Observables.

Examples

Classic Observer:

interface Observer {
  update(data: any): void;
}

interface Subject {
  attach(observer: Observer): void;
  detach(observer: Observer): void;
  notify(): void;
}

class NewsAgency implements Subject {
  private observers: Observer[] = [];
  private news = '';

  attach(observer: Observer) {
    if (!this.observers.includes(observer))
      this.observers.push(observer);
  }

  detach(observer: Observer) {
    const i = this.observers.indexOf(observer);
    if (i !== -1) this.observers.splice(i, 1);
  }

  notify() {
    this.observers.forEach(o => o.update(this.news));
  }

  setNews(news: string) {
    this.news = news;
    this.notify(); // Automatically notifies all observers
  }
}

class NewsChannel implements Observer {
  constructor(private name: string) {}
  update(news: string) {
    console.log(`${this.name} received: ${news}`);
  }
}

const agency = new NewsAgency();
const cnn = new NewsChannel('CNN');
const bbc = new NewsChannel('BBC');
agency.attach(cnn);
agency.attach(bbc);

agency.setNews('Major event!');
// ✅ CNN received: Major event!
// ✅ BBC received: Major event!

agency.detach(bbc);
agency.setNews('Economy update');
// ✅ Only CNN receives this

Event Emitter (Node.js style):

type EventHandler = (...args: any[]) => void;

class EventEmitter {
  private events = new Map<string, EventHandler[]>();

  on(event: string, handler: EventHandler) {
    if (!this.events.has(event)) this.events.set(event, []);
    this.events.get(event)!.push(handler);
  }

  off(event: string, handler: EventHandler) {
    const handlers = this.events.get(event);
    if (!handlers) return;
    const i = handlers.indexOf(handler);
    if (i !== -1) handlers.splice(i, 1);
  }

  emit(event: string, ...args: any[]) {
    this.events.get(event)?.forEach(h => h(...args));
  }

  once(event: string, handler: EventHandler) {
    const wrapper = (...args: any[]) => {
      handler(...args);
      this.off(event, wrapper);
    };
    this.on(event, wrapper);
  }
}

const emitter = new EventEmitter();
emitter.on('login', (name) => console.log(`Welcome ${name}!`));
emitter.once('firstVisit', (name) => console.log(`Tutorial for ${name}`));

emitter.emit('login', 'Alice');
emitter.emit('firstVisit', 'Alice');
emitter.emit('firstVisit', 'Alice'); // ✅ No output — once handler removed

Stock market tracker:

interface StockObserver {
  update(stock: string, price: number): void;
}

class StockMarket {
  private observers: StockObserver[] = [];
  private prices = new Map<string, number>();

  subscribe(observer: StockObserver) { this.observers.push(observer); }
  unsubscribe(observer: StockObserver) {
    const i = this.observers.indexOf(observer);
    if (i !== -1) this.observers.splice(i, 1);
  }

  setPrice(symbol: string, price: number) {
    const old = this.prices.get(symbol);
    this.prices.set(symbol, price);
    // Notify only on significant change (>1%)
    if (!old || Math.abs(price - old) / old > 0.01) {
      this.observers.forEach(o => o.update(symbol, price));
    }
  }
}

class PriceAlert implements StockObserver {
  constructor(private symbol: string, private target: number) {}
  update(stock: string, price: number) {
    if (stock === this.symbol && price >= this.target) {
      console.log(`ALERT: ${stock} reached $${price}!`);
    }
  }
}

class PortfolioTracker implements StockObserver {
  private holdings = new Map<string, number>();
  addHolding(symbol: string, shares: number) {
    this.holdings.set(symbol, shares);
  }
  update(stock: string, price: number) {
    const shares = this.holdings.get(stock);
    if (shares) {
      console.log(`${stock}: ${shares} shares = $${(shares * price).toFixed(2)}`);
    }
  }
}

Reactive state management (Redux-like):

type Subscriber<T> = (state: T) => void;

class Store<T> {
  private state: T;
  private subscribers: Subscriber<T>[] = [];

  constructor(initialState: T) { this.state = initialState; }

  getState(): T { return this.state; }

  setState(partial: Partial<T>) {
    this.state = { ...this.state, ...partial };
    this.subscribers.forEach(s => s(this.state));
  }

  subscribe(subscriber: Subscriber<T>): () => void {
    this.subscribers.push(subscriber);
    subscriber(this.state); // Initial notification
    // Return unsubscribe function
    return () => {
      const i = this.subscribers.indexOf(subscriber);
      if (i !== -1) this.subscribers.splice(i, 1);
    };
  }
}

interface AppState { user: string | null; count: number; }

const store = new Store<AppState>({ user: null, count: 0 });
const unsub = store.subscribe(state => console.log('State:', state));

store.setState({ user: 'Alice' });
store.setState({ count: 1 });
unsub(); // ✅ Stop listening
store.setState({ count: 2 }); // No output

Comparison

Use caseSuitableReason
One-to-many notificationsYesMultiple objects need updates
Loose couplingYesSubject does not know concrete observers
Event systems / UI eventsYesDOM events, custom events
Data binding / reactivityYesReact, Vue, spreadsheets
Update order mattersCautionObserver order is not guaranteed
Performance-critical with many observersCautionNotification overhead scales linearly
MechanismCouplingFlexibility
ObserverMedium — subject holds observer referencesDirect notification
Pub/SubLow — event channel decouples both sidesTopic filtering, async support
Reactive StreamsLow — declarative operatorsComposable, backpressure support

Interview guidance

How to explain it:

"Observer defines a one-to-many dependency so that when one object changes state, all dependents are notified automatically. It decouples the subject from its observers through a shared interface."

Common follow-ups:

QuestionKey points
Observer vs Pub/Sub?Observer: subject maintains observer references directly (tighter coupling). Pub/Sub: an event channel decouples publishers and subscribers (looser coupling, topic filtering). Pub/Sub is more flexible; Observer is simpler
Memory leak concerns?Observers prevent garbage collection if not detached. Solutions: (1) provide unsubscribe/detach. (2) Use weak references (WeakRef). (3) Automatic cleanup on component unmount (React useEffect cleanup, Angular ngOnDestroy). Always clean up observers
Where is Observer used in frameworks?React useState/useEffect, Angular Observable/Subject, Vue reactivity system, Node.js EventEmitter, DOM addEventListener

Difficulty: Intermediate


See also