| |

JavaScript 64 🧬 Design patterns

Design patterns are reusable solutions to common problems. They’re not algorithms — they’re templates for structuring code that have proven themselves across many projects. In JavaScript, some patterns are more relevant than others because the language handles certain things natively.

The patterns worth knowing in JavaScript fall into three groups: creational (how to make objects), structural (how to compose them), and behavioral (how they communicate). You don’t need all of them. You need the five or six that solve real problems in JavaScript — and you need to know when a pattern is overkill.

Key point: A design pattern is a vocabulary, not a rule. When you recognize “this is a singleton” or “this is an observer,” you can reason about the code faster. But if a simpler approach works, use it — patterns exist to solve problems, not to look impressive.


a – Creational patterns

Creational patterns deal with how objects are created. They hide the complexity of construction and let you swap implementations.

The Singleton pattern:

A singleton ensures a class has only one instance and provides a global point of access to it.

class Config {
  static #instance;

  constructor() {
    if (Config.#instance) return Config.#instance;
    this.settings = {};
    Config.#instance = this;
  }
}

const a = new Config();
const b = new Config();
console.log(a === b);

The constructor returns the existing instance if one exists. This is useful for shared resources — a database connection, a config object, a logger.

Why singletons can be dangerous: They’re global state. They make testing hard because you can’t create a fresh instance per test. Use them only when you truly need one instance.

The module pattern (ES modules as singletons):

Every ES module is a singleton by default. The first import evaluates it; later imports reuse the same instance:

// config.js
export const config = { theme: 'light' };

Anyone importing config gets the same object. This is the modern way to get singleton behavior without a class.

The Factory pattern:

A factory is a function that creates objects without exposing the construction logic:

function createUser(type) {
  if (type === 'admin') return { role: 'admin', permissions: ['all'] };
  if (type === 'guest') return { role: 'guest', permissions: ['read'] };
  throw new Error('Unknown type');
}

Instead of new AdminUser(), callers call createUser('admin'). The factory decides what to build. This is useful when the concrete type depends on runtime data.

The Builder pattern:

A builder constructs complex objects step by step:

class QueryBuilder {
  #parts = {};

  select(...fields) {
    this.#parts.select = fields.join(', ');
    return this;
  }

  from(table) {
    this.#parts.from = table;
    return this;
  }

  where(condition) {
    this.#parts.where = condition;
    return this;
  }

  build() {
    const { select = '*', from, where } = this.#parts;
    return `SELECT ${select} FROM ${from}` + (where ? ` WHERE ${where}` : '');
  }
}

const query = new QueryBuilder()
  .select('name', 'email')
  .from('users')
  .where('active = true')
  .build();

Each method returns this, allowing method chaining. The build() method produces the final object.

When to use creational patterns:

  • Singleton — shared config, logger, connection pool
  • Factory — when type depends on runtime data
  • Builder — when construction has many steps or options

When not to:

  • If new ClassName() is clear enough, don’t wrap it in a factory
  • If a plain object works, don’t build a class around it
  • If you only have one of something, consider just exporting it from a module

b – Structural patterns

Structural patterns deal with how objects are composed. They help you build larger structures from smaller ones.

The Module pattern:

Before ES modules, the module pattern used closures to hide private state:

const counter = (() => {
  let count = 0;
  return {
    increment: () => ++count,
    reset: () => (count = 0),
    get: () => count
  };
})();

The IIFE (immediately invoked function expression) creates a private scope. Only the returned object is exposed. count is not accessible from outside.

Modern alternative: ES modules provide the same thing natively:

// counter.js
let count = 0;
export const increment = () => ++count;
export const reset = () => (count = 0);
export const get = () => count;

Module scope replaces the IIFE. This is preferred in modern code.

The Decorator pattern:

A decorator wraps an object to add behavior without changing the original:

function withLogging(fn) {
  return (...args) => {
    console.log('calling with', args);
    const result = fn(...args);
    console.log('returned', result);
    return result;
  };
}

const add = (a, b) => a + b;
const loggedAdd = withLogging(add);

loggedAdd(2, 3);

This is function composition applied to wrapping. The original function is unchanged; the wrapper adds behavior.

Decorators on classes — a stage-3 proposal, not yet standard:

@logged
class MyClass {}

For now, use higher-order functions or Proxy for class-level decoration.

The Facade pattern:

A facade provides a simple interface over a complex system:

class VideoPlayer {
  play(file) {
    this.#loadCodec(file);
    this.#buffer(file);
    this.#render();
  }

  #loadCodec() {}
  #buffer() {}
  #render() {}
}

const player = new VideoPlayer();
player.play('movie.mp4');

The caller calls play(). The internal steps are hidden. This is everywhere — every library is essentially a facade over lower-level APIs.

The Adapter pattern:

An adapter makes one interface compatible with another:

class OldAPI {
  fetchData(callback) {
    setTimeout(() => callback({ data: 'old' }), 100);
  }
}

class APIAdapter {
  constructor(oldApi) {
    this.oldApi = oldApi;
  }
  fetch() {
    return new Promise(resolve => {
      this.oldApi.fetchData(data => resolve(data));
    });
  }
}

const api = new APIAdapter(new OldAPI());
await api.fetch();   // Promise-based

The adapter converts callback-style to Promise-style. This is common when integrating old libraries.

When to use structural patterns:

  • Module — hiding private state (or just use ES modules)
  • Decorator — adding behavior without modifying the original
  • Facade — hiding complexity behind a simple interface
  • Adapter — making incompatible interfaces work together

c – Behavioral patterns

Behavioral patterns deal with how objects communicate and distribute responsibility.

The Observer pattern:

An observer lets one object (the subject) notify many (the observers) when something changes.

class EventEmitter {
  #listeners = new Map();

  on(event, handler) {
    if (!this.#listeners.has(event)) this.#listeners.set(event, []);
    this.#listeners.get(event).push(handler);
    return () => this.off(event, handler);
  }

  off(event, handler) {
    const list = this.#listeners.get(event);
    if (!list) return;
    this.#listeners.set(event, list.filter(h => h !== handler));
  }

  emit(event, ...args) {
    (this.#listeners.get(event) || []).forEach(h => h(...args));
  }
}

const emitter = new EventEmitter();
const unsubscribe = emitter.on('data', d => console.log(d));

emitter.emit('data', 42);
unsubscribe();
emitter.emit('data', 99);

This is the pattern behind event listeners, Node.js EventEmitter, Vue reactivity, and Redux’s store subscription. Anything that needs to notify multiple listeners is an observer.

The Strategy pattern:

A strategy swaps algorithms at runtime:

const strategies = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b
};

function calculate(op, a, b) {
  return strategies[op](a, b);
}

Different behaviors behind the same interface. Used for sorting, formatting, pricing, and any case where “the algorithm” depends on context.

The Command pattern:

A command wraps an action as an object, letting you pass it around, queue it, or undo it:

class Command {
  constructor(execute, undo) {
    this.execute = execute;
    this.undo = undo;
  }
}

const commands = [];
const history = [];

function run(cmd) {
  cmd.execute();
  commands.push(cmd);
  history.push(cmd);
}

function undo() {
  const cmd = history.pop();
  if (cmd) cmd.undo();
}

Used in editors (undo/redo), task queues, and transaction systems.

The Mediator pattern:

A mediator centralizes communication between components:

class ChatRoom {
  #users = new Set();
  register(user) {
    this.#users.add(user);
    user.room = this;
  }
  broadcast(sender, message) {
    for (const u of this.#users) {
      if (u !== sender) u.receive(message);
    }
  }
}

class User {
  constructor(name) { this.name = name; }
  send(msg) { this.room.broadcast(this, msg); }
  receive(msg) { console.log(`${this.name} got: ${msg}`); }
}

Instead of every user talking to every other user, they all talk to the room. This is the pattern behind chat apps, message buses, and event aggregators.

The Iterator pattern:

JavaScript has this built in — the Symbol.iterator protocol:

const obj = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  }
};

[...obj];   // [1, 2, 3]

Any object with [Symbol.iterator] can be used in for...of, spread, and destructuring.

When to use behavioral patterns:

  • Observer — event systems, reactive state, notifications
  • Strategy — algorithm choice at runtime
  • Command — undo/redo, task queues, scheduling
  • Mediator — centralizing complex interactions
  • Iterator — custom iteration (or use generators)

When not to:

  • Don’t introduce an EventEmitter when a single callback works
  • Don’t build a command system unless you need undo or queueing
  • Don’t use a mediator when two components can talk directly

Complete Example Session

// ============================================
// PART 1: SINGLETON VIA MODULE
// ============================================

// config.js
let instance;
export function getConfig() {
  return instance ??= { theme: 'light' };
}

// main.js
import { getConfig } from './config.js';
const a = getConfig();
const b = getConfig();
// a === b

// ============================================
// PART 2: FACTORY
// ============================================

function createUser(type) {
  if (type === 'admin') return { role: 'admin', permissions: ['all'] };
  if (type === 'guest') return { role: 'guest', permissions: ['read'] };
  throw new Error('Unknown type');
}

console.log(createUser('admin'));
// [ { role: 'admin', permissions: [ 'all' ] } ]

// ============================================
// PART 3: BUILDER
// ============================================

class Query {
  #parts = {};
  select(...f) { this.#parts.select = f.join(','); return this; }
  from(t) { this.#parts.from = t; return this; }
  where(w) { this.#parts.where = w; return this; }
  build() {
    const { select = '*', from, where } = this.#parts;
    return `SELECT ${select} FROM ${from}${where ? ' WHERE ' + where : ''}`;
  }
}

console.log(new Query().select('name').from('users').where('id=1').build());
// [ 'SELECT name FROM users WHERE id=1' ]

// ============================================
// PART 4: MODULE PATTERN (IIFE)
// ============================================

const counter = (() => {
  let count = 0;
  return {
    increment: () => ++count,
    reset: () => (count = 0)
  };
})();

counter.increment();
counter.increment();
// count is 2, but private

// ============================================
// PART 5: DECORATOR
// ============================================

function withLogging(fn) {
  return (...args) => {
    console.log('args:', args);
    const result = fn(...args);
    console.log('result:', result);
    return result;
  };
}

const add = (a, b) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(2, 3);
// [ args: [ 2, 3 ] ]
// [ result: 5 ]

// ============================================
// PART 6: FACADE
// ============================================

class VideoPlayer {
  play(file) {
    this.#load(file);
    this.#buffer();
    this.#render();
  }
  #load() {}
  #buffer() {}
  #render() {}
}

// ============================================
// PART 7: ADAPTER
// ============================================

class OldAPI {
  fetchData(cb) {
    setTimeout(() => cb({ data: 'old' }), 100);
  }
}

class APIAdapter {
  constructor(api) { this.api = api; }
  fetch() {
    return new Promise(r => this.api.fetchData(r));
  }
}

// ============================================
// PART 8: OBSERVER
// ============================================

class EventEmitter {
  #listeners = new Map();
  on(event, fn) {
    if (!this.#listeners.has(event)) this.#listeners.set(event, []);
    this.#listeners.get(event).push(fn);
    return () => this.off(event, fn);
  }
  off(event, fn) {
    const list = this.#listeners.get(event) || [];
    this.#listeners.set(event, list.filter(h => h !== fn));
  }
  emit(event, ...args) {
    (this.#listeners.get(event) || []).forEach(fn => fn(...args));
  }
}

const bus = new EventEmitter();
const off = bus.on('ping', () => console.log('pong'));
bus.emit('ping');
// [ 'pong' ]
off();

// ============================================
// PART 9: STRATEGY
// ============================================

const strategies = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b
};

function calc(op, a, b) {
  return strategies[op](a, b);
}

console.log(calc('add', 2, 3));
// [ 5 ]

// ============================================
// PART 10: COMMAND
// ============================================

class Command {
  constructor(execute, undo) {
    this.execute = execute;
    this.undo = undo;
  }
}

const history = [];
const addCmd = new Command(
  () => console.log('adding'),
  () => console.log('removing')
);

addCmd.execute();
history.push(addCmd);
history.pop().undo();
// [ 'adding' ]
// [ 'removing' ]

// ============================================
// PART 11: MEDIATOR
// ============================================

class ChatRoom {
  #users = new Set();
  register(u) { this.#users.add(u); u.room = this; }
  broadcast(sender, msg) {
    for (const u of this.#users) {
      if (u !== sender) u.receive(msg);
    }
  }
}

class User {
  constructor(name) { this.name = name; }
  send(msg) { this.room.broadcast(this, msg); }
  receive(msg) { console.log(`${this.name}: ${msg}`); }
}

const room = new ChatRoom();
const alice = new User('Alice');
const bob = new User('Bob');
room.register(alice);
room.register(bob);
alice.send('hello');
// [ Bob: hello ]

// ============================================
// PART 12: ITERATOR
// ============================================

const range = {
  from: 1,
  to: 3,
  *[Symbol.iterator]() {
    for (let i = this.from; i <= this.to; i++) yield i;
  }
};

console.log([...range]);
// [ [ 1, 2, 3 ] ]

// ============================================
// PART 13: COMBINING PATTERNS
// ============================================

class Store extends EventEmitter {
  #state = {};

  get(key) { return this.#state[key]; }
  set(key, value) {
    this.#state[key] = value;
    this.emit('change', key, value);
  }
}

const store = new Store();
store.on('change', (k, v) => console.log(`${k} = ${v}`));
store.set('theme', 'dark');
// [ theme = dark ]

// ============================================
// PART 14: WHEN NOT TO USE A PATTERN
// ============================================

// Don't need a factory for one type
function makeUser(name) {
  return { name };
}

// Don't need an observer for one listener
element.addEventListener('click', handler);

// Don't need a class for a plain object
const config = { theme: 'light' };

// Don't need a builder for two fields
const point = { x: 1, y: 2 };

Quick Reference

Creational

PatternPurposeJS Idiom
SingletonOne instanceModule export
FactoryCreate by typeFunction returning object
BuilderStep-by-step constructionChained methods

Structural

PatternPurposeJS Idiom
ModulePrivate stateIIFE, ES modules
DecoratorAdd behaviorHigher-order function
FacadeSimple interfaceWrapper class
AdapterConvert interfaceWrapper with new API

Behavioral

PatternPurposeJS Idiom
ObserverNotify manyEventEmitter, addEventListener
StrategySwap algorithmsObject of functions
CommandEncapsulate actionObject with execute/undo
MediatorCentralize commsEvent bus, store
IteratorCustom iterationSymbol.iterator

Pattern vs Modern JS

PatternOften replaced by
SingletonES modules
ModuleES modules
ObserverEventTarget, EventEmitter
IteratorGenerators
StrategyFunctions / map lookup
CommandClosures

When to Use

Use patterns whenSkip them when
You recognize the shapeA function would do
The problem is commonThe solution is obvious
The team shares vocabularyThe code is one-off
Flexibility is neededSimplicity is enough

Best Practices

Do This:

// Use ES modules for singletons
export const config = { theme: 'light' };

// Use closures for private state
const counter = (() => { let n = 0; return { inc: () => ++n }; })();

// Use higher-order functions for decorators
const logged = fn => (...a) => { console.log(a); return fn(...a); };

// Use EventTarget for observation
const bus = new EventTarget();
bus.addEventListener('ping', handler);

// Use plain functions for strategy
const ops = { add: (a, b) => a + b };

// Use generators for iteration
function* range(n) { for (let i = 0; i < n; i++) yield i; }

// Compose patterns sparingly
class Store extends EventEmitter { ... }

Don’t Do This:

// Don't force a singleton when a module works
class Singleton { ... }   // ⚠️  overkill

// Don't build a factory for one type
function makeUser() { return new User(); }   // ⚠️  needless

// Don't decorate when you can extend
const logged = fn => (...) => { ... };      // ✅
// Not every function needs a wrapper

// Don't hide complexity behind a facade if it's already simple
class SimpleWrapper { doIt() { return doIt(); } }  // ❌

// Don't use the observer pattern for a single callback
emitter.on('done', cb);      // ⚠️  a callback would do

// Don't build a command system unless you need undo/queue

// Don't use a mediator when two components can talk directly

// Don't repeat patterns for their own sake — they cost code

Common Pitfalls

PitfallProblemSolution
Singleton everywhereHard to testUse modules, inject deps
Factory for one typeOverheadDirect construction
Observer for one listenerExtra codeUse a callback
Adapter sprawlLayers of wrappingSimplify interfaces
Decorator chainsHard to debugLimit nesting
Strategy objectsMore code than if/elseUse only for real choice
Command without undoPremature abstractionSkip it
Mediator as god objectHidden couplingSplit responsibilities

Real-World Examples

1. Module singleton

// logger.js
const logs = [];
export const logger = {
  info: (msg) => logs.push(msg),
  get: () => [...logs]
};

One instance shared across imports — the modern singleton.

2. Factory by type

function createShape(type) {
  const shapes = {
    circle: r => ({ type: 'circle', r, area: () => Math.PI * r ** 2 }),
    square: s => ({ type: 'square', s, area: () => s ** 2 })
  };
  return shapes[type](...arguments.length > 1 ? Array.prototype.slice.call(arguments, 1) : []);
}

Picks a creator by name — common in parsers and serializers.

3. Builder for query

const sql = new QueryBuilder()
  .select('id', 'name')
  .from('users')
  .where('active = true')
  .build();

Reads like the SQL it generates.

4. Decorator for caching

function memoize(fn) {
  const cache = new Map();
  return (...args) => {
    const key = JSON.stringify(args);
    if (!cache.has(key)) cache.set(key, fn(...args));
    return cache.get(key);
  };
}

const slow = memoize(n => n * n);

Wraps a function with caching — transparent.

5. Adapter for old APIs

const fs = require('fs').promises;
// Callback-style fs.promises → Promise
const readFile = path => new Promise((res, rej) =>
  require('fs').readFile(path, (e, d) => e ? rej(e) : res(d))
);

Converts callback to Promise.

6. Observer with EventTarget

const bus = new EventTarget();
bus.addEventListener('user-login', e => console.log(e.detail));

bus.dispatchEvent(new CustomEvent('user-login', { detail: { id: 1 } }));

EventTarget is a built-in observer.

7. Strategy for sort

const sorters = {
  name: (a, b) => a.name.localeCompare(b.name),
  age: (a, b) => a.age - b.age
};

users.sort(sorters[field]);

Same interface, swappable algorithms.

8. Command for undo

const history = [];
function execute(cmd) {
  cmd.do();
  history.push(cmd);
}
function undo() {
  const c = history.pop();
  if (c) c.undo();
}

Standard editor pattern.

9. Mediator for chat

class ChatRoom {
  #users = [];
  broadcast(sender, msg) {
    this.#users.forEach(u => u !== sender && u.receive(msg));
  }
}

Centralizes communication — no direct user-to-user references.

10. Iterator with generator

function* range(from, to) {
  for (let i = from; i <= to; i++) yield i;
}

[...range(1, 5)];

The iterator pattern made trivial by generators.


Visual: The Three Categories

┌──────────────────────────────────────────────┐
│  CREATIONAL                                  │
│                                              │
│  How objects are made                        │
│                                              │
│  Singleton  ──► one instance                 │
│  Factory    ──► build by type                │
│  Builder    ──► step by step                 │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  STRUCTURAL                                  │
│                                              │
│  How objects are composed                    │
│                                              │
│  Module    ──► hide internals                │
│  Decorator ──► wrap and extend               │
│  Facade    ──► simplify interface            │
│  Adapter   ──► convert interface             │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  BEHAVIORAL                                  │
│                                              │
│  How objects communicate                     │
│                                              │
│  Observer  ──► notify many                   │
│  Strategy  ──► swap algorithms               │
│  Command   ──► wrap actions                  │
│  Mediator  ──► centralize                    │
│  Iterator  ──► custom traversal              │
│                                              │
└──────────────────────────────────────────────┘

Visual: Observer Pattern

┌──────────────────────────────────────────────┐
│  Subject (EventEmitter)                      │
│                                              │
│  ├── listener 1                              │
│  ├── listener 2                              │
│  └── listener 3                              │
│                                              │
│  emit('event') ──► calls all listeners       │
│                                              │
└──────────────────────────────────────────────┘

  Listener 1 ──► gets data
  Listener 2 ──► gets data
  Listener 3 ──► gets data

  The subject doesn't know what the listeners do
  The listeners don't know about each other

Visual: Strategy Pattern

┌──────────────────────────────────────────────┐
│  calculate(op, a, b)                         │
│       │                                      │
│       ▼                                      │
│  ┌──────────────┐                            │
│  │  op = 'add'  │──► (a, b) => a + b         │
│  │  op = 'sub'  │──► (a, b) => a - b         │
│  │  op = 'mul'  │──► (a, b) => a * b         │
│  └──────────────┘                            │
│                                              │
│  Same call site — different algorithm        │
│                                              │
└──────────────────────────────────────────────┘

Summary

CategoryPatternUse for
CreationalSingletonOne shared instance
CreationalFactoryCreate by type
CreationalBuilderMulti-step construction
StructuralModulePrivate state
StructuralDecoratorAdd behavior
StructuralFacadeHide complexity
StructuralAdapterConvert interfaces
BehavioralObserverNotify many
BehavioralStrategySwap algorithms
BehavioralCommandEncapsulate actions
BehavioralMediatorCentralize comms
BehavioralIteratorCustom traversal

Key takeaways:

  • Design patterns are shared solutions to common problems — vocabulary more than rules
  • Creational patterns handle how objects are made: Singleton, Factory, Builder
  • Structural patterns handle composition: Module, Decorator, Facade, Adapter
  • Behavioral patterns handle communication: Observer, Strategy, Command, Mediator, Iterator
  • JavaScript has built-in versions of several patterns — ES modules are singletons, generators are iterators, EventTarget is an observer
  • Observer is the most-used pattern — event systems, reactive state, notifications
  • Strategy replaces long if/else chains with swappable functions
  • Builder shines when construction has many optional steps
  • Decorator wraps without modifying — the basis of middleware, HOCs, and memoization
  • Prefer the simplest pattern that solves the problem — often a plain function or object
  • Don’t over-engineer — patterns cost code; if you don’t need the flexibility, skip them
  • Combine patterns — a Store built on EventEmitter is observer + facade
  • Recognize the shape — once you name it, you understand the code

Remember: Design patterns are a shared language. When you spot an observer, you know what it does. When you see a factory, you know the type is decided at runtime. But patterns exist to solve problems, not to be used. The best code uses the minimum pattern required — often none. Learn the six or seven that matter in JavaScript, use them when they fit, and skip them when they don’t. Master patterns, and you stop reinventing solutions.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!