JavaScript

Design Patterns

15 Questions

The Singleton pattern ensures a class has exactly one instance throughout the application lifecycle, providing a global access point to that single object. In JavaScript, ES modules are inherently singletons—each module is evaluated once and cached by the runtime. Class-based Singletons guard against repeated instantiation using a static private field that stores and returns the first created instance.
class Database {
  static #instance = null;

  constructor(url) {
    if (Database.#instance) return Database.#instance;
    this.url = url;
    Database.#instance = this;
  }
}

const db1 = new Database('localhost');
const db2 = new Database('remote');
console.log(db1 === db2); // true
console.log(db2.url);     // "localhost" — first instance wins

// Simplest approach: ES module singleton (db.js)
// export const db = new Database('localhost');
// Every importer shares the same instance automatically

Why it matters: Singletons appear in nearly every production app for shared resources—misimplementing them leads to duplicate connections, inconsistent state, and race conditions that are hard to debug.

Real applications: Mongoose database connection pool (Node.js), Redux store, Angular root-level services (providedIn root), and browser localStorage are all Singleton instances shared across the entire app.

Common mistakes: Re-implementing class-based Singleton when importing an ES module already provides singleton behavior for free—every named module export is effectively a Singleton, making manual static instance tracking redundant boilerplate.

The Observer pattern (also called Pub/Sub) allows objects to subscribe to events and be notified automatically when those events occur. A central subject maintains a list of listener callbacks; when state changes it iterates and invokes each one. This decouples event producers from consumers, making the architecture extensible without modifying the emitter.
class EventEmitter {
  #events = {};

  on(event, fn) {
    (this.#events[event] ??= []).push(fn);
    return () => this.off(event, fn); // returns unsubscribe fn
  }

  off(event, fn) {
    this.#events[event] = this.#events[event]?.filter(f => f !== fn);
  }

  emit(event, ...args) {
    this.#events[event]?.forEach(fn => fn(...args));
  }
}

const bus = new EventEmitter();
const unsub = bus.on('data', (msg) => console.log('Received:', msg));
bus.emit('data', 'Hello World'); // "Received: Hello World"
unsub();                         // clean up listener
bus.emit('data', 'Again');       // silence — unsubscribed

Why it matters: Observer is fundamental to event-driven programming; interviewers test it to see if you understand decoupling and whether you handle listener cleanup to prevent memory leaks.

Real applications: Node.js EventEmitter, DOM addEventListener, RxJS Observables (used heavily in Angular), and Redux store subscription callbacks all implement the Observer pattern.

Common mistakes: Forgetting to call the returned unsubscribe function when a component unmounts, causing stale callbacks to fire long after the subscriber is destroyed and producing memory leaks.

The Factory pattern centralizes object creation in a dedicated function or class, returning different types based on input without exposing constructor details to the caller. It respects the Open/Closed Principle—adding new product types extends the factory without modifying consuming code. In JavaScript, factory functions are often preferred over new because they are simpler, more flexible, and easier to test.
function createUser(role) {
  const base = { role, createdAt: Date.now() };
  switch (role) {
    case 'admin':  return { ...base, canDelete: true,  canBan: true };
    case 'editor': return { ...base, canPublish: true, canDelete: false };
    case 'viewer': return { ...base, readOnly: true };
    default: throw new Error(`Unknown role: ${role}`);
  }
}

const admin  = createUser('admin');
const viewer = createUser('viewer');
console.log(admin.canDelete);  // true
console.log(viewer.readOnly);  // true

// Adding a new role only requires a new case —
// no changes needed in any consuming code

Why it matters: Interviewers use the Factory pattern to test knowledge of the Open/Closed Principle and whether you can decouple object creation from business logic to make systems extensible and testable.

Real applications: React.createElement(), Angular component factories, Express router factory, and Knex.js query builder all use the Factory pattern to abstract construction complexity behind a stable function interface.

Common mistakes: Letting callers use new directly on concrete classes instead of going through the factory, which creates tight coupling that makes swapping implementations difficult without touching every call site.

The Module pattern uses closures (via IIFE) or ES module files to encapsulate private state and expose only a curated public API. This prevents global namespace pollution and enforces information hiding—private variables and helper functions are inaccessible from outside the module boundary. Modern JavaScript favors ES modules over IIFEs because they are statically analyzable and tree-shakeable.
// IIFE Module — encapsulates private state
const Counter = (() => {
  let count = 0; // private — not accessible outside

  return {
    increment() { return ++count; },
    decrement() { return --count; },
    reset()     { count = 0; },
    getCount()  { return count; }
  };
})();

Counter.increment(); // 1
Counter.increment(); // 2
Counter.getCount();  // 2
// Counter.count     // undefined — private!

// ES Module equivalent (counter.js)
// let count = 0;
// export const increment = () => ++count;
// export const getCount  = () => count;

Why it matters: Understanding the Module pattern demonstrates mastery of closures and scope—two of the most-tested JavaScript fundamentals—and shows how encapsulation is achieved without class syntax.

Real applications: jQuery's entire codebase was wrapped in an IIFE; Webpack and Rollup bundle ES modules using this concept; Node.js CommonJS wraps each file in a function providing closure-based private scope.

Common mistakes: Believing properties on the returned public object are private—only variables declared in the closure scope are truly private; properties of the returned object can be freely read and modified by outside code.

The Strategy pattern defines a family of interchangeable algorithms and lets the client select which one to use at runtime without changing the context code. In JavaScript, functions are first-class citizens, making strategies naturally expressible as plain functions stored in a lookup object or passed as arguments—no abstract classes needed. This eliminates brittle if/else chains and satisfies the Open/Closed Principle.
// Payment strategies as a lookup table
const paymentStrategies = {
  creditCard: (amount) => `Charging $${amount} to credit card`,
  paypal:     (amount) => `Sending $${amount} via PayPal`,
  crypto:     (amount) => `Broadcasting $${amount} in BTC`
};

function checkout(amount, strategy) {
  const fn = paymentStrategies[strategy];
  if (!fn) throw new Error(`Unknown strategy: ${strategy}`);
  return fn(amount);
}

checkout(100, 'paypal');     // "Sending $100 via PayPal"
checkout(100, 'creditCard'); // "Charging $100 to credit card"

// Adding a new method requires no changes elsewhere
paymentStrategies.applePay = (amount) => `Apple Pay: $${amount}`;

Why it matters: Strategy is a top interview pattern because it directly demonstrates the Open/Closed Principle—interviewers want to see you replace conditionals with a data-driven dispatch table that supports extension without modification.

Real applications: Passport.js (pluggable authentication strategies), Webpack loader selection, Lodash sort comparators, and form validation libraries all use this pattern to support pluggable, swappable behaviors.

Common mistakes: Using a switch statement instead of a lookup object—switch is harder to extend and cannot be modified at runtime, whereas a plain object allows dynamic strategy registration and deletion.

The Decorator pattern wraps a function or object to transparently extend its behavior without modifying the original implementation. In JavaScript, higher-order functions are the natural mechanism—a decorator takes a function as input, wraps it in a new function with added behavior (logging, caching, retry logic), and returns the enhanced version. TC39's decorator proposal (Stage 3) brings native class decorator syntax to JavaScript.
// Decorator adding retry logic to any async function
function withRetry(fn, retries = 3) {
  return async function(...args) {
    for (let attempt = 0; attempt < retries; attempt++) {
      try {
        return await fn.apply(this, args);
      } catch (err) {
        if (attempt === retries - 1) throw err;
        console.warn(`Retry ${attempt + 1}/${retries}`);
      }
    }
  };
}

// Decorator adding memoization
function memoize(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (!cache.has(key)) cache.set(key, fn.apply(this, args));
    return cache.get(key);
  };
}

// Stack decorators — memoize then wrap with retries
const fetchUser = withRetry(memoize(getUserById), 3);

Why it matters: Decorators are a core interview topic because they demonstrate mastery of higher-order functions and cross-cutting concerns—logging, caching, authentication, and retry logic in production are almost always implemented as decorators.

Real applications: Angular uses class decorators extensively (@Component, @Injectable); NestJS mirrors this with @Controller; React Higher-Order Components (HOCs) are the Decorator pattern applied to UI components.

Common mistakes: Forgetting to use fn.apply(this, args) inside the wrapper, which breaks methods that rely on the correct this context and loses the original function's name from stack traces.

The JavaScript Proxy object intercepts fundamental operations on a target—property reads, writes, deletions, and function calls—through customizable trap handlers. Unlike the abstract Proxy design pattern, JavaScript has native language-level support, enabling reactive data binding, input validation, and access control without modifying the original object. The essential companion is Reflect, which provides safe default trap behavior.
function createValidator(target, schema) {
  return new Proxy(target, {
    set(obj, prop, value) {
      if (prop in schema && !schema[prop](value)) {
        throw new TypeError(`Invalid value for "${prop}": ${value}`);
      }
      obj[prop] = value;
      return true; // required: must return true on success
    },
    get(obj, prop) {
      return Reflect.get(obj, prop); // delegate default behavior
    }
  });
}

const user = createValidator({}, {
  age:  (v) => Number.isInteger(v) && v >= 0,
  name: (v) => typeof v === 'string' && v.length > 0
});

user.name = 'Alice'; // OK
user.age  = 30;      // OK
// user.age = -1;    // TypeError: Invalid value for "age": -1

Why it matters: Proxy is a senior-level interview topic testing deep JavaScript knowledge—it shows understanding of meta-programming and how modern frameworks implement reactivity and validation under the hood.

Real applications: Vue 3's reactivity system is built entirely on Proxy; MobX 5+ uses Proxy for observable objects; Immer uses Proxy to track mutations and produce immutable state updates.

Common mistakes: Forgetting to return true from the set trap in strict mode, which silently causes a TypeError; and not using Reflect methods inside traps, which can break invariants for non-configurable or non-writable properties.

The Iterator pattern defines a standardized way to traverse a sequence without exposing the collection's internal structure. JavaScript formalizes this with the iteration protocol—any object with a [Symbol.iterator]() method returning a { next() } iterator is iterable and works natively with for...of, spread, and destructuring. Generators using function* are the most concise way to implement custom iterators.
class Range {
  constructor(start, end) { this.start = start; this.end = end; }

  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    return {
      next() {
        return current <= end
          ? { value: current++, done: false }
          : { value: undefined, done: true };
      }
    };
  }
}

for (const n of new Range(1, 5)) console.log(n); // 1 2 3 4 5
console.log([...new Range(1, 3)]); // [1, 2, 3]

// Generator equivalent — much more concise
function* range(start, end) {
  for (let i = start; i <= end; i++) yield i;
}
console.log([...range(1, 5)]); // [1, 2, 3, 4, 5]

Why it matters: The iterator protocol underpins generators, async iteration, and all for...of loops—understanding it is essential for working with custom data structures, lazy sequences, and streaming data pipelines.

Real applications: Node.js readable streams implement async iterators; MongoDB driver cursors use async iteration; D3.js data joins rely on iterability; Immer draft patches iterate over change sets.

Common mistakes: Implementing Symbol.iterator but returning the object itself as the iterator without a proper next() method, which breaks iteration when the same iterable is used in nested loops simultaneously.

The Mediator pattern centralizes communication between components so they interact through a single mediator object rather than holding direct references to each other. This reduces n×(n−1) direct dependencies between n components down to n dependencies on the mediator, making the system drastically easier to maintain and extend. The mediator knows all participants; participants know only the mediator.
class EventBus {
  #handlers = new Map();

  subscribe(event, handler) {
    if (!this.#handlers.has(event)) this.#handlers.set(event, []);
    this.#handlers.get(event).push(handler);
    return () => this.unsubscribe(event, handler); // returns unsub fn
  }

  unsubscribe(event, handler) {
    const list = this.#handlers.get(event) ?? [];
    this.#handlers.set(event, list.filter(h => h !== handler));
  }

  publish(event, payload) {
    this.#handlers.get(event)?.forEach(h => h(payload));
  }
}

const bus = new EventBus();
// Components talk through the bus — never directly to each other
const unsubCart  = bus.subscribe('item:added', ({ name }) => console.log(`Cart: +${name}`));
const unsubBadge = bus.subscribe('item:added', ({ count }) => console.log(`Badge: ${count}`));
bus.publish('item:added', { name: 'Laptop', count: 1 });
unsubCart(); // only badge handler receives events now

Why it matters: The Mediator is the architectural foundation of event-driven UIs—interviewers test it to see if you can design loosely coupled systems where adding a feature doesn't require modifying existing components.

Real applications: Redux (the store mediates all state updates), Angular NgRx action bus, DOM CustomEvent dispatching, and Socket.io rooms all act as mediators coordinating multiple components.

Common mistakes: Letting the mediator grow into a God Object that accumulates business logic—the mediator should only coordinate communication routing, not contain domain logic that belongs inside individual components.

The Command pattern encapsulates a request as a self-contained object that includes all information needed to execute the action and optionally reverse it. This enables undo/redo stacks, operation queuing, macro recording, and audit logging without the invoker knowing anything about the receiver's internals. Each command implements a consistent execute and undo interface.
class History {
  #stack = [];

  execute(command) {
    command.execute();
    this.#stack.push(command);
  }

  undo() { this.#stack.pop()?.undo(); }
}

class SetValueCommand {
  constructor(obj, key, newValue) {
    this.obj      = obj;
    this.key      = key;
    this.newValue = newValue;
    this.oldValue = obj[key]; // snapshot old value for undo
  }
  execute() { this.obj[this.key] = this.newValue; }
  undo()    { this.obj[this.key] = this.oldValue; }
}

const state   = { color: 'red' };
const history = new History();

history.execute(new SetValueCommand(state, 'color', 'blue'));
console.log(state.color); // "blue"
history.execute(new SetValueCommand(state, 'color', 'green'));
console.log(state.color); // "green"
history.undo();
console.log(state.color); // "blue"

Why it matters: Command is a crucial pattern for any app requiring undo/redo—interviewers use it to test if you can design composable, reversible operations rather than directly mutating state in place.

Real applications: VS Code's edit history, Figma's undo stack, Redux dispatch actions (each action is a Command object), and database transaction logs all implement the Command pattern for reversible operations.

Common mistakes: Not capturing the previous state as a snapshot at command construction time—if the old value is read lazily at undo time, concurrent mutations between execute and undo produce incorrect rollbacks.

The Builder pattern constructs complex objects incrementally through a fluent interface where each method configures one aspect and returns this for chaining, with a terminal call to execute the result. This replaces constructors with many optional parameters (the "telescoping constructor" anti-pattern) with clear, self-documenting method calls. It is especially valuable when objects have many optional, order-independent configuration options.
class RequestBuilder {
  #config = { method: 'GET', headers: {}, body: null, timeout: 5000 };

  url(url)     { this.#config.url = url; return this; }
  method(m)    { this.#config.method = m.toUpperCase(); return this; }
  header(k, v) { this.#config.headers[k] = v; return this; }
  body(data)   { this.#config.body = JSON.stringify(data); return this; }
  timeout(ms)  { this.#config.timeout = ms; return this; }

  async send() {
    const { url, method, headers, body, timeout } = this.#config;
    const ctrl = new AbortController();
    const id   = setTimeout(() => ctrl.abort(), timeout);
    try {
      return await fetch(url, { method, headers, body, signal: ctrl.signal });
    } finally {
      clearTimeout(id);
    }
  }
}

const res = await new RequestBuilder()
  .url('https://api.example.com/users')
  .method('POST')
  .header('Authorization', 'Bearer token')
  .body({ name: 'Alice' })
  .timeout(3000)
  .send();

Why it matters: Builder demonstrates fluent API design—a skill interviewers look for in senior developers who design SDKs, test fixtures, or configuration-heavy objects that resist argument-ordering bugs and are self-documenting at call sites.

Real applications: Knex.js SQL query builder, Jest assertion chaining, Mongoose query chains (.find().sort().limit()), and Axios request configuration all implement the Builder pattern's fluent interface.

Common mistakes: Mutating the same internal config object across multiple uses of the same builder instance—if the builder is reused without resetting, previous configuration options silently leak into subsequent builds.

The Adapter pattern converts the interface of an existing component into the interface expected by the client, enabling two incompatible APIs to work together without modifying either side. It acts as a translation wrapper—the client calls the adapter using its familiar interface, and the adapter delegates to the wrapped component's actual API. This is critical when integrating or migrating between third-party libraries.
// Third-party logger with a verbose API
class WinstonLogger {
  log(level, message, meta) {
    console.log(`[${level.toUpperCase()}] ${message}`, meta);
  }
}

// App expects a simple { info, warn, error } interface
class LoggerAdapter {
  #logger;
  constructor(logger) { this.#logger = logger; }
  info(msg, meta)  { this.#logger.log('info',  msg, meta ?? {}); }
  warn(msg, meta)  { this.#logger.log('warn',  msg, meta ?? {}); }
  error(msg, meta) { this.#logger.log('error', msg, meta ?? {}); }
}

const logger = new LoggerAdapter(new WinstonLogger());
logger.info('Server started', { port: 3000 });
// [INFO] Server started { port: 3000 }

// Swap to a different logger with no app code changes:
// const logger = new LoggerAdapter(new DatadogLogger());

Why it matters: Adapters are how production codebases safely integrate external libraries—interviewers use this pattern to assess whether you can isolate third-party dependencies so they can be swapped without causing ripple-effect changes.

Real applications: Axios browser/Node.js adapters, Passport.js authentication strategy adapters, TypeORM database driver adapters, and React's synthetic event system (adapts native browser events) all use this pattern.

Common mistakes: Embedding business logic inside the adapter—adapters should only translate interfaces, not add feature behavior; mixing concerns turns a simple adapter into a hidden source of bugs that violates the single-responsibility principle.

Memoization is an optimization pattern that caches return values of pure functions keyed by their input arguments, so repeated calls with identical inputs return instantly from cache without recomputing. It is a specific application of the Decorator pattern—a memoize wrapper adds a transparent cache layer to any pure function without altering its behavior or interface.
function memoize(fn, maxSize = Infinity) {
  const cache = new Map();
  return function memoized(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn.apply(this, args);
    // Evict oldest entry when cache exceeds size limit
    if (cache.size >= maxSize) cache.delete(cache.keys().next().value);
    cache.set(key, result);
    return result;
  };
}

// Recursive memoization — must reference the memoized outer var
const fib = memoize(function fibonacci(n) {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2); // hits cache on recursive calls
});

console.time('first');  fib(40); console.timeEnd('first');  // ~2ms
console.time('second'); fib(40); console.timeEnd('second'); // ~0ms

Why it matters: Memoization is a core performance optimization that interviewers expect you to implement from scratch—it appears in algorithm problems (top-down DP), React's useMemo/useCallback, and API response caching.

Real applications: React useMemo and React.memo, Reselect (Redux selector memoization), Lodash _.memoize, and Vue 3 computed properties all use memoization to skip redundant computation when inputs haven't changed.

Common mistakes: Applying memoization to impure functions—functions with side effects or non-deterministic output like Date.now() or Math.random()—which causes the cache to return stale or incorrect results on subsequent calls.

The Facade pattern provides a simplified, unified interface over a complex subsystem of classes or APIs, hiding internal implementation details from the client. It does not prevent direct access to the subsystem—it simply offers a convenient high-level API that covers the most common use cases with sensible defaults. Facades reduce coupling between client code and the underlying complexity.
// Complex subsystem
class HttpClient  { request(cfg) { return fetch(cfg.url, cfg); } }
class TokenStore  { getToken()   { return localStorage.getItem('token'); } }
class RateLimiter { check(url)   { /* enforce limits */ return true; } }
class Logger      { log(msg)     { console.log('[API]', msg); } }

// Facade — single clean entry point over all subsystems
class ApiClient {
  #http    = new HttpClient();
  #tokens  = new TokenStore();
  #limiter = new RateLimiter();
  #logger  = new Logger();

  get(url)        { return this.#request('GET', url); }
  post(url, body) { return this.#request('POST', url, body); }

  #request(method, url, body) {
    if (!this.#limiter.check(url)) throw new Error('Rate limited');
    this.#logger.log(`${method} ${url}`);
    return this.#http.request({
      url, method,
      headers: { Authorization: `Bearer ${this.#tokens.getToken()}` },
      body: body ? JSON.stringify(body) : undefined
    });
  }
}

const api = new ApiClient();
api.get('/users');          // handles auth, logging, rate-limiting
api.post('/users', data);   // same simplicity

Why it matters: Facade is one of the most practically useful patterns—interviewers look for it when discussing API client design, SDK abstraction, or hiding third-party complexity behind a clean internal interface that's easy to swap.

Real applications: jQuery (facade over the DOM API), Axios (facade over XMLHttpRequest/Fetch), Mongoose (facade over the MongoDB driver), and AWS SDK clients are all facades that hide low-level protocol complexity.

Common mistakes: Over-relying on the facade until it stops covering an edge case, then reaching through it to call the subsystem directly—this defeats the abstraction and creates hidden coupling that makes the facade misleading.

The Chain of Responsibility pattern passes a request along an ordered sequence of handlers, where each handler processes the request, transforms it, or forwards it to the next handler in the chain. This decouples the sender from receivers and dynamically composes processing pipelines. It is the structural basis of Express.js middleware, DOM event propagation, and HTTP interceptors.
class Pipeline {
  #middlewares = [];

  use(fn) { this.#middlewares.push(fn); return this; }

  async run(ctx) {
    let index = 0;
    const next = async () => {
      if (index < this.#middlewares.length) {
        await this.#middlewares[index++](ctx, next);
      }
    };
    await next();
    return ctx;
  }
}

const pipeline = new Pipeline()
  .use(async (ctx, next) => {
    ctx.startTime = Date.now();
    await next();
    ctx.duration = Date.now() - ctx.startTime; // post-handler timing
  })
  .use(async (ctx, next) => {
    if (!ctx.token) { ctx.error = '401 Unauthorized'; return; } // stop chain
    await next();
  })
  .use(async (ctx, next) => {
    ctx.result = `Processed: ${ctx.input}`;
  });

const ctx = await pipeline.run({ token: 'abc', input: 'hello' });
console.log(ctx.result);   // "Processed: hello"
console.log(ctx.duration); // ~0ms

Why it matters: Chain of Responsibility is the architectural pattern behind middleware stacks that every Node.js developer uses daily—interviewers test it to see if you understand layered request processing and where each concern belongs.

Real applications: Express.js and Koa middleware stacks, Angular HttpClient interceptors, Axios request/response interceptors, and browser service workers (fetch event chain) all implement this pattern.

Common mistakes: Forgetting to call next() in middleware that should not short-circuit the chain—silently dropping a request without calling next or sending a response is the most common cause of hanging API calls in Express apps.