Design Patterns in JavaScript
Expert · 18 min read · ▶ live playground · ✦ checkpoint
Design patterns in JavaScript are recurring code shapes you can recognize, adapt, and discuss. They are not trophies to collect. In this lesson, you will build module, factory, observer/pub-sub, and strategy patterns in plain JavaScript, then spot the same ideas in language features and libraries you already use.
Patterns are vocabulary, not ceremony
A design pattern is a name for a useful shape that keeps appearing in real programs. The value is the shortcut it gives your brain: "this code hides private state behind a public API", "this code lets observers subscribe to events", or "this code swaps one algorithm for another."
JavaScript changes how many classic patterns look. Functions are values, closures are normal, objects are lightweight, modules are built in, and many library APIs accept callbacks. That means the idiomatic JavaScript version of a pattern is often smaller than the class-heavy diagram you might see in an old design-patterns book.
Start with the module pattern: keep details private, expose a small public surface.
function createCounterModule(label) {
let count = 0;
return {
increment() {
count += 1;
return count;
},
snapshot() {
return { label, count };
},
};
}
const clicks = createCounterModule("checkout");
clicks.increment();
clicks.increment();
console.log("count", clicks.snapshot().count);
console.log("hidden", clicks.count);→ count 2
→ hidden undefined — count lives in the closure, not on the returned object.
In a real multi-file project, ES modules are your normal file boundary: export the public things, keep helpers unexported. The closure version above is still useful because it creates a private module-like instance at runtime.
Try it — four patterns in one cart
This playground uses four patterns without turning them into a museum. Read it once for behavior, then read it again for shape:
createEventBusis a factory that returns an observer/pub-sub module.createCartis a factory that returns a cart module with privateitems.discountStrategiesis a strategy table: each function answers the same discount question differently.unsubscribeis the cleanup habit you learned while profiling leaks.
The important move is not memorizing the names. The important move is noticing where variation lives. The cart owns item state. The event bus owns subscriptions. Discount rules are just functions with the same contract. When a new VIP rule appears, you add or swap a strategy; you do not bury a long conditional inside the cart.
Patterns the language absorbed
Some patterns become so common that JavaScript grows a standard shape for them.
The iterator pattern used to mean an object with a "give me the next value" method. JavaScript absorbed that idea into the iteration protocol: Symbol.iterator, next(), for...of, spread, destructuring, generators, and iterator helpers all speak that shared shape.
The promise pattern used to mean "a value that will arrive later." JavaScript absorbed that idea into Promise, .then, .catch, .finally, async/await, and promise combinators. You still need the mental model, but you normally use the language's built-in object instead of inventing your own future-value wrapper.
The decorator pattern means "wrap something to add behavior while preserving its core job." In JavaScript, you can do that today with ordinary functions:
function withAudit(label, action) {
return function auditedAction(input) {
console.log(`before ${label}`);
const result = action(input);
console.log(`after ${label}`);
return result;
};
}
const saveTitle = withAudit("save", (title) => title.trim().toUpperCase());
console.log(saveTitle(" draft "));→ before save
→ after save
→ DRAFT
You will also see @decorator-style syntax in some framework and tooling conversations. Treat that as a project-support question, not as proof that every JavaScript runtime you target accepts the syntax. The pattern is stable; the exact syntax support depends on the stack in front of you.
Recognizing patterns in libraries
Once you know the shapes, library APIs become easier to read.
A function named createClient, createRouter, createStore, or configureSomething is probably a factory. It gathers configuration once and returns a ready object or set of functions.
An API with .subscribe(...), .on(...), .off(...), .emit(...), addEventListener(...), or a returned cleanup function is probably observer/pub-sub. Something registers interest, something publishes a fact, and cleanup prevents duplicate work or memory leaks.
An options object that accepts callbacks often hides a strategy. Array sorting uses a comparator strategy. Retry helpers accept backoff strategies. Test runners accept reporter strategies. HTTP clients accept adapter strategies. The point is the same: the stable code owns the workflow, and your function supplies the interchangeable rule.
A package with a small public export surface and many unexported helpers is using the module idea. You do not need to see the word "module" in a README to recognize the boundary.
The professional move is to name patterns lightly in reviews: "This looks like pub/sub; where do we unsubscribe?" or "This discount rule wants to be a strategy." That is useful. Saying "we need an AbstractStrategyFactoryManager" before the code asks for it is not.
Design patterns are a reading skill as much as a writing skill. Next, we put taste around that skill: SOLID, DRY, YAGNI, composition, module edges, and naming without turning any of them into dogma.
Checkpoint
Answer all three to mark this lesson complete