Every experienced programmer keeps a mental drawer of shapes that worked before: a function that builds objects so you stop repeating the literal, a single shared instance everyone reaches through one door, a subject that announces its changes to anyone listening. These shapes are design patterns — names for recurring solutions to recurring problems. They are not rules you must follow, and they are not specific to JavaScript; they are shared vocabulary. When a teammate says the logging service is a singleton or the shipping cost uses a strategy, you both know exactly what they mean.
This lesson walks through four of the most common patterns in everyday JavaScript code: the factory that builds objects, the singleton that guards one shared instance, the observer that broadcasts changes, and the strategy that swaps algorithms behind one interface. None of them need a class or a library. Each is a few lines and a clear idea.
flowchart TD F["factory createUser"] --> A["object: Ada, admin"] F --> B["object: Lin, guest"] F --> C["object: Grace, editor"] style F fill:#3776ab,color:#fff style A fill:#1e7f3d,color:#fff style B fill:#1e7f3d,color:#fff style C fill:#1e7f3d,color:#fff
The factory: build, do not repeat
Writing the same object literal in five places is a smell, because the day the shape changes you have five edits to make and five chances to miss one. A factory is a function whose only job is to construct and return an object. Callers ask the factory for what they need and never touch the new keyword or retype the literal.
The factory centralises construction in one place. Add a default, validate an argument, or attach a method, and every caller benefits at once. The objects it returns are independent: two calls give you two separate objects, so a change to one never leaks into the other. That property alone makes factories the default way to create objects in most modern JavaScript code.
// A FACTORY builds and returns an object, so callers never write `new`.
function createUser(name, role) {
return {
name: name,
role: role,
describe() {
return this.name + " is a " + this.role;
},
};
}
const admin = createUser("Ada", "admin");
const guest = createUser("Lin", "guest");
console.log(admin.describe()); // Ada is a admin
console.log(guest.role); // guest
The singleton: one shared instance
Sometimes you genuinely want exactly one of something — one configuration object, one counter for the whole program, one source of a particular value. A singleton guarantees that every caller receives the same instance, no matter how many times they ask for it.
JavaScript implements this cleanly with a closure. A function holds a private variable; on the first call it builds the instance and stores it, and on every later call it hands back that stored object. Because the variable lives inside the closure, no other code can reach it directly — the only way in is through the function, which is what makes the guarantee trustworthy. The trade-off is real, though: that one instance is shared mutable state, and shared mutable state is where surprising bugs breed.
flowchart TD A["caller A asks getInstance"] --> I["the one instance"] B["caller B asks getInstance"] --> I C["caller C asks getInstance"] --> I style I fill:#b45309,color:#fff style A fill:#3776ab,color:#fff style B fill:#3776ab,color:#fff style C fill:#3776ab,color:#fff
// A SINGLETON keeps ONE instance in a closure and hands it out each time.
const makeCounter = (() => {
let instance = null;
return () => {
if (!instance) {
instance = { count: 0, bump() { this.count += 1; return this.count; } };
}
return instance;
};
})();
const a = makeCounter();
const b = makeCounter();
console.log(a === b); // true — same instance
console.log(a.bump()); // 1
console.log(b.bump()); // 2 — a and b share state
The observer: announce your changes
When one object needs to react to another, the heavy-handed approach is to poll: ask over and over whether anything has changed. The observer pattern inverts that. The subject keeps a list of interested functions — its subscribers — and calls each one when something worth knowing happens. The subject does not know or care what the subscribers do; it only promises to tell them. That decoupling is why user interfaces, event systems, and reactive libraries are all built on this single idea.
The strategy: swap the algorithm
The strategy pattern solves the sprawling if/else that picks behaviour. Instead of branching on a flag, you hand in a function that knows how to do the job, and the caller picks which one. Want a different shipping rule or a different sort order? Pass a different strategy. The interface stays fixed while the behaviour behind it changes — a clean seam that keeps growth from turning into tangles.
flowchart LR S["subject<br/>state changes"] --> N["notify all"] N --> SA["subscriber A"] N --> SB["subscriber B"] N --> SC["subscriber C"] style S fill:#3776ab,color:#fff style N fill:#b45309,color:#fff style SA fill:#1e7f3d,color:#fff
// OBSERVER: a subject keeps a list of subscribers and notifies them.
function makeStore() {
let subscribers = [];
let stock = 0;
return {
subscribe(fn) { subscribers.push(fn); },
restock(n) {
stock = n;
for (const fn of subscribers) fn(stock);
},
};
}
const store = makeStore();
const log = [];
store.subscribe((q) => log.push("A sees " + q));
store.subscribe((q) => log.push("B sees " + q));
store.restock(5);
console.log(log); // ["A sees 5","B sees 5"]
Write a factory makeProduct(name, price) that returns an object with name, price, and a method summary() returning the string <name>: $<price> — so makeProduct("Mug", 10).summary() gives Mug: $10. Each call must return a fresh object.
function makeProduct(name, price) {
// return a fresh object with name, price, and summary()
}
Return an object literal with name, price, and a summary method.
summary uses this.name and this.price so it reads the object's own fields.
function makeProduct(name, price) {
return {
name: name,
price: price,
summary() { return this.name + ": $" + this.price; },
};
}
Write makeLoggerSingleton() that returns a getLogger function. getLogger always returns the SAME logger object — a singleton guarded by a closure. The logger has a messages array and a log(msg) method that pushes onto it. Two calls to the returned getLogger give the very same object, so a message logged through one is visible through the other.
function makeLoggerSingleton() {
// hold ONE instance in a closure; getLogger returns it every time
return function getLogger() {
// your code here
};
}
Keep an
instancevariable in the outer closure, null until first use.On the first getLogger call, build the object and store it; later calls return the stored one.
function makeLoggerSingleton() {
let instance = null;
return function getLogger() {
if (!instance) {
instance = { messages: [], log(msg) { this.messages.push(msg); } };
}
return instance;
};
}
This event emitter lets callers subscribe with on(fn) and broadcast with emit(value). Each subscriber should receive the emitted value, but emit calls the functions with no argument at all — so every subscriber sees undefined. Fix emit so it passes value to every subscribed function.
function makeEmitter() {
let subs = [];
return {
on(fn) { subs.push(fn); },
emit(value) {
for (const fn of subs) fn(); // BUG: forgets to pass the value
},
};
}
The loop already visits every subscriber; it just drops the value.
Pass value into each call: fn(value).
function makeEmitter() {
let subs = [];
return {
on(fn) { subs.push(fn); },
emit(value) {
for (const fn of subs) fn(value);
},
};
}
The strategy pattern swaps the algorithm behind a single interface. Write createShipping(strategy) where strategy is a function (weight) => cost. The returned object has a method costFor(weight) that delegates entirely to the chosen strategy. Callers pick a different rule — flat fee, by-weight, and so on — by passing a different function, with no if/else anywhere.
function createShipping(strategy) {
// return an object whose costFor(weight) uses the strategy
}
costFor does no arithmetic itself — it hands the weight to the strategy.
return strategy(weight); is the whole method body.
function createShipping(strategy) {
return {
costFor(weight) {
return strategy(weight);
},
};
}
Which statement best describes the strategy pattern?
Strategy is about interchangeable algorithms behind a common interface — you change behaviour by swapping the function you pass in. The other choices describe observer, singleton, and factory respectively.
Recap
- A factory is a function that builds and returns an object, centralising construction so callers never repeat the literal.
- A singleton uses a closure to guarantee one shared instance, reached through a single access function.
- An observer lets a subject notify a list of subscribers when its state changes, without knowing what they do.
- A strategy swaps the algorithm behind a fixed interface by passing a different function in, replacing
if/elsebranching. - All four hide some state; singletons and observers share mutable state, so build in a way to reset or unsubscribe.
These patterns close out the module — the tools for writing JavaScript that stays readable as it grows.