Command Palette

Search for a command to run...

Hectal
PHASE 15Advanced ~28 min· topic 2 of 8

Topic 15.2

Design Patterns in JavaScript

In one line

Design patterns are named, proven shapes for common code problems; in JavaScript the everyday ones are the module (private state), observer and pub/sub (notify listeners), factory (build the right object), singleton (one shared instance) and strategy (swap the algorithm).

Think of it like this

A school. The office keeps the register locked away and only answers through the window (module: private state, public answers). The notice board lets anyone pin a notice and anyone read it, without the writer and readers ever meeting (pub/sub). A parent who subscribed to the class group gets every message from that teacher (observer). The uniform shop hands you the right uniform when you say your class (factory). There is one principal everyone talks to (singleton). And for getting home you pick bus, cycle or walk, but "getting home" stays the same job (strategy).

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Design pattern
A named, reusable solution to a problem that keeps appearing in code.
API
The functions and properties that a piece of code lets others use; everything else stays hidden.
IIFE
Immediately Invoked Function Expression: a function that is written and run in one go, used to make private scope.
Observer
A function that has asked to be called whenever a subject changes or an event happens.
Pub/sub
Publish/subscribe: senders post messages to named topics through a middleman; listeners subscribe to topics.
Factory
A function that decides which kind of object to create and creates it.
Singleton
A design where only one instance of something exists and everyone shares it.
Strategy
One of several interchangeable algorithms that can be swapped without changing the code that calls it.
Coupling
How much one part of the code depends on the details of another. Loose coupling means few dependencies.

Step by step

01Hide state with the module pattern

The IIFE runs once and returns an object. count lives only in the closure, so the only way to change it is through increment and reset. Nobody can set it to -5 by accident.

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

02Let others react with an observer

The subject stores listeners in a Set (so the same function can't be added twice) and calls them on emit. on returns a function that removes the listener again.

Let others react with an observerdiagram
Rendering diagram…

03Decouple with pub/sub

With a bus in the middle, the cart button doesn't know about the badge or the log. Each one talks only to the bus, by topic name. Add a fourth widget without touching the other three.

Decouple with pub/subdiagram
Rendering diagram…

04Centralise creation with a factory

All the rules for building a shape live in one place: which types exist, what each needs, and what to do with an unknown type (throw a clear error). Callers never use new on six different classes.

05Swap behaviour with a strategy

Instead of if (method === "express") ... else if ..., keep the options in an object and look one up. Adding "drone delivery" is one new line in the object; quote never changes.

shipping.jswhole filejavascript
const shipping = {
  standard: (kg) => 40 + kg * 10,
  express: (kg) => 100 + kg * 25,
};
const quote = (kg, method) => shipping[method](kg);

06Know when a module is the singleton

Every file that imports config.js gets the same object, because a module runs only once per page. That is the simplest singleton in JavaScript, and it needs no pattern code at all.

config.jswhole filejavascript
export const config = { theme: "dark", apiBase: "/api" };
// a.js and b.js both: import { config } from "./config.js";  -> the same object

Try it yourself

  1. 1

    Add a third shipping option

    In the second example, add drone: (kg) => 200 to shipping. Predict the new third line before running. Did quote need any change?

  2. 2

    Add a once() method

    In the first example, add once(event, fn) to Emitter: it subscribes a wrapper that calls fn and then removes itself. Emit twice and check the listener runs once.

  3. 3

    Add a subscriber

    In the pub/sub example, subscribe a third function that turns the badge red once the count reaches 3. Which existing code did you have to change?

Code & diagrams

Module pattern and an observer with unsubscribe ES2022+ New tab

`counter.count` is `undefined`: the variable is private to the closure. `Emitter {}` looks empty because `#private` fields are not ordinary properties.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

2 undefined
A saw 10
B saw 10
B saw 12
Emitter {}
Factory, strategy and singleton New tab

Changing the config through `b` changed it for `a` too: there is only one instance.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

9 13
Unknown shape: blob
standard=60 express=150 pickup=0
true light
Pub/sub: widgets that never meet New tab

Click the button yourself: the badge and the list update, and neither subscriber knows the button exists.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

log: Book 1
log: Book 2

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Unsubscribe with a different function

In the first example, replace stopA(); with store.off("price", (p) => console.log("A saw", p));.

terminal
$ Console
── what you'll see ──
2 undefined
A saw 10
B saw 10
A saw 12
B saw 12
Emitter {}

Break #2

Ask a factory for something it can't make

Call createShape("triangle", 2).area() without a try/catch.

terminal
$ Console
── what you'll see ──
Uncaught Error: Unknown shape: triangle

Myth vs fact

Myth

Design patterns need classes.

Fact

Most of them are shorter with functions and closures in JavaScript. A strategy is often an object of functions; a factory is a function that returns an object.

Myth

Observer and pub/sub are the same thing.

Fact

In observer, the subject holds its listeners directly and they subscribe to it. In pub/sub, a broker sits between, and publishers and subscribers only share a topic name.

Myth

Using more patterns makes code better.

Fact

A pattern is worth it when it removes real repetition or coupling. Wrapping a two-line function in a factory and a singleton adds indirection with no benefit.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The browser ships a ready-made observer: class Store extends EventTarget {} gets addEventListener, removeEventListener and dispatchEvent(new CustomEvent("change", { detail })), plus { once: true } and { signal } for removal with an AbortController.

  • ▸

    Emitters that store listeners in an array and loop over it while a listener unsubscribes itself can skip the next listener. Iterate over a copy ([...listeners]), or rely on Set iteration semantics, which visits items added during iteration and skips deleted ones.

  • ▸

    Class singletons hurt testing because state survives between tests; module singletons have the same problem in test runners that share a module cache. Prefer passing dependencies in (dependency injection) and creating the one instance at the app's entry point.

  • ▸

    Proxies (Topic 14.6) plus observers give you reactivity: Vue 3 wraps state in a Proxy whose set trap notifies subscribers, which is the observer pattern applied automatically.

Remember this

  1. 1

    A design pattern is a reusable answer to a problem that keeps coming up, with a name so a team can talk about it ("let's use an observer here"). The famous 1994 "Gang of Four" book lists 23 for class-based languages. JavaScript has first-class functions, closures and object literals, so many patterns become a few lines, and some are built into the language. Hectal's /system-design course covers all 23 in depth; this topic shows how the five you'll meet most look in real JavaScript.

  2. 2

    The module pattern gives you private state and a public API (the set of functions others may call). Before ES modules (2015) people used an IIFE (Immediately Invoked Function Expression: (function () { ... })()), a function that runs once; its local variables live on in a closure and only the returned object can reach them. Today an ES module file does the same job naturally: anything you don't export is private (Phase 12), and inside classes #private fields (ES2022) do it per object.

  3. 3

    The observer pattern: a subject keeps a list of observers (listener functions) and calls each one when something happens. addEventListener on a button is exactly this (Phase 9). A good observer API returns an unsubscribe function or offers off, because a listener you forget to remove keeps its closure alive forever: a memory leak (Topic 14.8). Pub/sub (publish/subscribe) adds a middleman, a broker or event bus: publishers send to a named topic and subscribers listen to the topic, so neither knows the other exists. That loose coupling is great for independent widgets and dangerous when overused, because it gets hard to see who reacts to what.

  4. 4

    The factory pattern puts "which object do I build?" in one function. Callers say what they want (createShape("circle", 2)) and the factory picks the right constructor or object shape, fills in defaults and validates input. Factory functions are also JavaScript's simplest alternative to classes: a function that returns an object, with private data held in closures instead of #fields.

  5. 5

    The singleton pattern guarantees one shared instance: one app config, one connection, one cache. In JavaScript an ES module is already a singleton (a module runs once and every importer gets the same exports), so export const config = { ... } is usually all you need. A class-based singleton keeps a static #instance and hands it back. Use singletons sparingly: they are global state in disguise, which makes tests leak into each other.

  6. 6

    The strategy pattern keeps a family of interchangeable algorithms behind one interface, so the caller can switch between them at runtime. In JavaScript that is often an object of functions ({ standard, express, pickup }) or a function passed as an argument; array.sort(compareFn) is a strategy. It replaces long if/else or switch chains and lets you add a new option without touching the code that uses it (the open/closed principle: open to extension, closed to modification).

Explain it without notes

01

How do you get private state in JavaScript? Give three ways.

02

What is the difference between the observer pattern and pub/sub?

03

Why can a singleton be a problem, and what does JavaScript give you instead?

04

Explain the strategy pattern with an example from the standard library.

Practice

01

Write a factory function createAccount(owner) that keeps balance private and returns deposit, withdraw (refusing to go below 0 by throwing) and getBalance.

02

Add a once(event, fn) method to the Emitter class from the first example.

03

Replace this switch with a strategy object: function discount(type, price) { switch (type) { case "student": return price * 0.5; case "senior": return price * 0.7; default: return price; } }.

Trade-offs

  • ↔

    Pub/sub decouples parts of an app, but the flow of control becomes invisible: to know what happens on cart:add you have to search for every subscriber.

  • ↔

    Classes with #private fields give clear structure and fast, memory-friendly methods on the prototype; closure-based factories are simpler and truly private but create new function objects per instance.

Done when you can

  • I can write a module with private state using a closure or #private fields.

  • I can build an emitter with on, off, emit and an unsubscribe function.

  • I can explain observer versus pub/sub.

  • I can write a factory and a strategy object, and explain when each helps.

  • I know why ES modules already act as singletons and why global state is risky.