Module 8 · Functional & Advanced Patterns ⏱ 18 min

Design Patterns in JavaScript

By the end of this lesson you will be able to:
  • Use a factory function to build objects without repeating object literals
  • Implement a singleton with a closure so every caller receives the same instance
  • Apply the observer and strategy patterns to decouple and swap behaviour

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
A factory is one builder that produces many independent objects from the same shape.

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 fresh objects. Each call returns its own independent instance.
// 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 hands every caller the very same instance through a single access function.
A closure-guarded singleton. Two calls return the same object and share its state.
// 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
An observer: the subject notifies every subscriber when its state changes, without knowing what they do.
An observer emitter broadcasting to its subscribers.
// 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"]
Exercise

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()
}
Exercise

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
  };
}
Exercise

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
    },
  };
}
Exercise

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
}
Exercise

Which statement best describes the strategy pattern?

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/else branching.
  • 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.

Checkpoint quiz

A factory is best described as:

Why does the singleton pattern require care?

Go deeper — technical resources