Engineering

JavaScript Design Patterns That Scale

Twelve essential creational, structural, and behavioral patterns — when to reach for each, and how they show up in real product code.

View reference repoOpen live demo

Design patterns are shared vocabulary for structure. In JavaScript they are less about class hierarchies and more about controlling creation, adapting interfaces, and coordinating change — without painting yourself into a corner.

This post distills the lessons from our interactive tutorial, available as an open-source reference at learn_javascript-design-patterns and live at learn-js-design-patterns.org. Each pattern includes runnable examples you can step through in the browser.

Why Patterns Matter in JavaScript

Classic Gang-of-Four patterns were written for statically typed, class-centric languages. JavaScript offers a different toolkit: first-class functions, closures, modules, and prototype-based inheritance. The intent of each pattern — encapsulate variation, isolate side effects, decouple collaborators — still applies. The implementation often looks like a factory function and a module boundary rather than an abstract base class.

Patterns are not prescriptions. They are trade-offs you can name in code review, design docs, and onboarding. When a teammate says “this looks like a Facade over three services,” everyone understands the boundary being drawn.

Core insight

In JavaScript, prefer composition over inheritance and explicit module boundaries over deep class trees. Patterns give you repeatable shapes for those boundaries.

A Map of the Twelve Patterns

The tutorial organizes patterns into three categories. Use this map to decide where a problem belongs before choosing a specific solution.

Creational — how objects come into existence

  • Singleton — one shared instance (config, connection pool)
  • Factory — delegate instantiation to a creator
  • Builder — assemble complex objects step by step
  • Prototype — clone from an existing instance

Structural — how pieces fit together

  • Adapter — translate one interface into another
  • Decorator — add behavior without subclassing
  • Facade — simplify a subsystem behind one entry point
  • Proxy — control access to an object (lazy load, caching)

Behavioral — how objects communicate

  • Observer — publish/subscribe when state changes
  • Mediator — centralize coordination between components
  • Strategy — swap algorithms at runtime
  • Command — encapsulate actions as objects (undo, queue)

Representative Patterns in Depth

Five patterns cover the majority of real-world JavaScript architecture decisions. For the remaining seven, follow the same when/when-not framing in the tutorial repo.

Singleton — shared state with guardrails

A Singleton ensures only one instance exists. In Node and browser apps this often appears as a module-level export: the module system itself guarantees a single evaluation.

// config.js — module singleton (idiomatic in JS)
let instance;
export function getConfig() {
  if (!instance) {
    instance = Object.freeze({ apiUrl: process.env.API_URL });
  }
  return instance;
}

Reach for it when you need one authoritative source (telemetry client, feature-flag reader). Avoid it when testability suffers — global mutable singletons make unit tests order-dependent. Prefer dependency injection in large codebases.

Factory — hide construction details

Factories centralize object creation so callers depend on an interface, not concrete constructors. This is the default pattern for plugin systems, payment providers, and environment-specific adapters.

function createNotifier(type) {
  switch (type) {
    case "email": return new EmailNotifier();
    case "slack": return new SlackNotifier();
    default: throw new Error(`Unknown notifier: ${type}`);
  }
}

Reach for it when creation logic branches on config or runtime context. Avoid it when you only ever construct one type — a plain constructor is simpler and easier to trace.

Adapter — integrate without rewriting

Adapters wrap a third-party API or legacy module so the rest of your application speaks a stable internal contract. This is essential at system boundaries: CRM integrations, payment gateways, analytics SDKs.

// Wrap legacy callback API in a Promise-based interface
export function fetchUser(id) {
  return new Promise((resolve, reject) => {
    legacyApi.getUser(id, (err, user) =>
      err ? reject(err) : resolve(mapToUser(user))
    );
  });
}

Reach for it at integration seams where you cannot change the upstream API. Avoid it when you own both sides — fix the interface directly instead of adding a translation layer.

Observer — react to change without tight coupling

Observers subscribe to a subject; when state changes, subscribers are notified. In modern frontends this appears as event emitters, RxJS streams, and framework reactivity. The pattern decouples producers from consumers.

class EventBus {
  #listeners = new Map();
  on(event, fn) {
    const set = this.#listeners.get(event) ?? new Set();
    set.add(fn);
    this.#listeners.set(event, set);
  }
  emit(event, payload) {
    this.#listeners.get(event)?.forEach((fn) => fn(payload));
  }
}

Reach for it when many components need the same signal (cart updates, auth state). Avoid it when a simple callback or direct prop pass suffices — uncontrolled pub/sub creates debugging pain (“who fired this event?”).

Strategy — interchangeable algorithms

Strategy encapsulates a family of algorithms behind a common interface. Callers select the implementation at runtime. Pricing rules, validation pipelines, and export formats are natural fits.

Reach for it when business rules vary by tenant, region, or feature flag. Avoid it when there is only one algorithm today and no credible second variant — YAGNI applies.

When a pattern earns its place

  • The problem repeats across features or teams
  • Tests become simpler after introducing the boundary
  • Onboarding docs can name the pattern in one sentence

When to skip it

  • Only one implementation exists with no roadmap for variation
  • The abstraction obscures the data flow
  • You are optimizing for a hypothetical future requirement

Anti-Patterns to Watch For

  • Pattern stacking — Factory that returns a Singleton wrapped in a Proxy. Each layer should solve a concrete problem, not demonstrate pattern literacy.
  • God Singleton — one module that holds app state, HTTP client, and logger. Split responsibilities or inject dependencies.
  • Observer sprawl — dozens of anonymous listeners with no ownership model. Document event contracts and enforce unsubscribe on teardown.
  • Adapter permanence — keeping a shim years after the legacy system is retired. Schedule removal when the migration completes.

Takeaways Checklist

  1. Name the category first — creation, structure, or behavior — before picking a pattern.
  2. Prefer module exports and functions over class hierarchies unless polymorphism is required.
  3. Apply patterns at boundaries: integrations, plugin points, and cross-cutting concerns.
  4. Write the “when not” case alongside the “when” case in design reviews.
  5. Use the interactive tutorial to compare implementations side by side before committing to an approach in production code.

Patterns are tools, not trophies. The goal is code that a new engineer can modify safely six months from now — with vocabulary that makes design intent visible in the source.