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.
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.
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.
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.
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.
export const config = { theme: "dark", apiBase: "/api" };
// a.js and b.js both: import { config } from "./config.js"; -> the same objectTry it yourself
- 1
Add a third shipping option
In the second example, add
drone: (kg) => 200toshipping. Predict the new third line before running. Didquoteneed any change? - 2
Add a once() method
In the first example, add
once(event, fn)toEmitter: it subscribes a wrapper that callsfnand then removes itself. Emit twice and check the listener runs once. - 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
`counter.count` is `undefined`: the variable is private to the closure. `Emitter {}` looks empty because `#private` fields are not ordinary properties.
Preview
Console
Nothing logged yet.Expected console output
2 undefined
A saw 10
B saw 10
B saw 12
Emitter {}Changing the config through `b` changed it for `a` too: there is only one instance.
Preview
Console
Nothing logged yet.Expected console output
9 13
Unknown shape: blob
standard=60 express=150 pickup=0
true lightClick the button yourself: the badge and the list update, and neither subscriber knows the button exists.
Preview
Console
Nothing logged yet.Expected console output
log: Book 1
log: Book 2Break 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));.
Break #2
Ask a factory for something it can't make
Call createShape("triangle", 2).area() without a try/catch.
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 {}getsaddEventListener,removeEventListeneranddispatchEvent(new CustomEvent("change", { detail })), plus{ once: true }and{ signal }for removal with anAbortController. - ▸
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 onSetiteration 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
Proxywhosesettrap notifies subscribers, which is the observer pattern applied automatically.
Remember this
- 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-designcourse covers all 23 in depth; this topic shows how the five you'll meet most look in real JavaScript. - 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'texportis private (Phase 12), and inside classes#privatefields (ES2022) do it per object. - 3
The observer pattern: a subject keeps a list of observers (listener functions) and calls each one when something happens.
addEventListeneron a button is exactly this (Phase 9). A good observer API returns an unsubscribe function or offersoff, 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
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
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 astatic #instanceand hands it back. Use singletons sparingly: they are global state in disguise, which makes tests leak into each other. - 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 longif/elseorswitchchains 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
How do you get private state in JavaScript? Give three ways.
What is the difference between the observer pattern and pub/sub?
Why can a singleton be a problem, and what does JavaScript give you instead?
Explain the strategy pattern with an example from the standard library.
Practice
Write a factory function createAccount(owner) that keeps balance private and returns deposit, withdraw (refusing to go below 0 by throwing) and getBalance.
Add a once(event, fn) method to the Emitter class from the first example.
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:addyou have to search for every subscriber. - ↔
Classes with
#privatefields 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
#privatefields.I can build an emitter with
on,off,emitand 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.