Design Patterns

Reviewed & published by Brayan K

Master proven solutions to common programming problems with battle-tested design patterns.

Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

💡 Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:

🏗️ Real-World Analogy: Architectural Blueprints

Think of design patterns like architectural blueprints for houses. Just as architects don't redesign plumbing from scratch for every house (they use proven patterns), software developers use design patterns to solve recurring problems. A "kitchen layout pattern" works whether you're building a cottage or a mansion — similarly, the "Observer pattern" works whether you're building a chat app or a stock ticker.

Patterns aren't code you copy-paste — they're proven strategies you adapt to your specific needs.

📋 Which Pattern Should I Use?

ProblemPatternReal Example
Need only one instance globallySingletonDatabase connection, Logger
Create different objects based on typeFactoryUser types (Admin, Guest)
Notify multiple objects of changesObserverEvent listeners, React state
Build complex objects step by stepBuilderQuery builders, form builders
Add features without changing coreDecoratorMiddleware, HOCs in React

What Are Design Patterns?

Design patterns are reusable, battle-tested solutions to common programming problems. They are not specific pieces of code you must copy, but structured ideas you can implement in many ways using JavaScript.

You can think of them like:

In this expert-level lesson, you'll learn:

1. Creational Patterns — How to Build Objects

Creational patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation.

Singleton

Ensures only one instance of a class exists.

Factory

Creates objects without specifying the exact class to create.

class Car {
    constructor(model) {
        this.model = model;
    }
}

class CarFactory {
    create(model) {
        return new Car(model);
    }
}

const factory = new CarFactory();
const myCar = factory.create("Tesla");
console.log(myCar.model);

Builder

Separates the construction of a complex object from its representation.

class QueryBuilder {
    constructor() {
        this.query = {
            select: [],
            from: null,
            where: []
        };
    }

    select(fields) {
        this.query.select = fields;
        return this;
    }

    from(table) {
        this.query.from = table;
        return this;
    }

    where(condition) {
        this.query.where.push(condition);
        return this;
    }

    build() {
        return this.query;
    }
}

const query = new QueryBuilder()
    .select(['id', 'name'])
    .from('users')
    .where('age > 18')
    .build();

console.log(query);

🎯 Your Turn — Build a Singleton and a Factory

The three demos above are your worked examples. Now write the two creational patterns you will reach for most often, on a different problem. Everything is pre-written except three blanks, and each blank is the one line that makes the pattern a pattern.

// 🎯 YOUR TURN - finish the Singleton and the Factory
// Everything is written for you except the three blanks marked ___

// PART 1 - SINGLETON
// A settings store that must exist exactly once in the whole app.
class Settings {
  constructor() {
    // 👉 1) If an instance already exists, hand THAT one back instead of a new one.
    //       Replace ___ with the property name used three lines below.
    if (Settings.___) return Settings.instance;

    this.values = {};
    Settings.instance = this;      // remember this first instance forever
  }
  set(key, value) { this.values[key] = value; return this; }
  get(key) { return this.values[key]; }
}

const a = new Settings();
a.set("theme", "dark");
const b = new Settings();          // looks like it builds a second object...

console.log(b.get("theme"));       // ...but it is the very same one
console.log(a === b);

// PART 2 - FACTORY
// One function decides WHICH class to build, so callers never write 'new'.
class EmailNotifier { send(msg) { return "EMAIL: " + msg; } }
class SmsNotifier   { send(msg) { return "SMS: " + msg; } }
class PushNotifier  { send(msg) { return "PUSH: " + msg; } }

function createNotifier(kind) {
  if (kind === "email") return new EmailNotifier();

  // 👉 2) Build and return an SmsNotifier here, the same way the line above does.
  if (kind === "sms")   return ___;

  if (kind === "push")  return new PushNotifier();

  // 👉 3) An unknown kind must not quietly return undefined.
  //       Replace ___ with the keyword that raises the error.
  ___ new Error("Unknown notifier: " + kind);
}

console.log(createNotifier("email").send("Welcome"));
console.log(createNotifier("sms").send("Code 4821"));
console.log(createNotifier("push").send("You have 2 updates"));

try {
  createNotifier("carrier-pigeon");
} catch (err) {
  console.log("Caught:", err.message);
}

// ✅ Expected output:
// dark
// true
// EMAIL: Welcome
// SMS: Code 4821
// PUSH: You have 2 updates
// Caught: Unknown notifier: carrier-pigeon

Blank 3 matters more than it looks. A factory that returns undefined for a typo pushes the bug somewhere far away, where the error message will not mention the factory at all. Failing loudly at the point of the mistake is part of the pattern.

2. Structural Patterns — How to Compose Objects

Structural patterns explain how to assemble objects to form larger structures, keeping these structures flexible and efficient.

Module

Encapsulates a part of an application into a single unit.

const calculator = (() => {
    const add = (a, b) => a + b;
    const subtract = (a, b) => a - b;

    return {
        add: add,
        subtract: subtract
    };
})();

console.log(calculator.add(5, 3));
console.log(calculator.subtract(5, 3));

Decorator

Dynamically adds responsibilities to an object.

class Coffee {
    getCost() {
        return 5;
    }

    getDescription() {
        return "Simple coffee";
    }
}

class MilkDecorator {
    constructor(coffee) {
        this.coffee = coffee;
    }

    getCost() {
        return this.coffee.getCost() + 2;
    }

    getDescription() {
        return this.coffee.getDescription() + ", milk";
    }
}

const coffee = new Coffee();
const milkCoffee = new MilkDecorator(coffee);

console.log(milkCoffee.getCost());
console.log(milkCoffee.getDescription());

Facade

Provides a simplified interface to a complex subsystem.

class CPU {
    freeze() { return "CPU Freeze"; }
    jump(position) { return "CPU Jump"; }
    execute() { return "CPU Execute"; }
}

class Memory {
    load(position) { return "Memory Load"; }
}

class HardDrive {
    read(lba, size) { return "HardDrive Read"; }
}

class ComputerFacade {
    constructor() {
        this.cpu = new CPU();
        this.memory = new Memory();
        this.hardDrive = new HardDrive();
    }

    start() {
        this.cpu.freeze();
        this.memory.load("0x00");
        this.cpu.jump("0x00");
        this.cpu.execute();
    }
}

const computer = new ComputerFacade();
computer.start();

// The point of a facade: ONE call replaced four low-level ones.
console.log("Computer started with a single method call.");
console.log("Behind that one call: " + computer.cpu.freeze() + ", " + computer.memory.load("0x00") + ", " + computer.cpu.jump("0x00") + ", " + computer.cpu.execute());

// Expected output:
// Computer started with a single method call.
// Behind that one call: CPU Freeze, Memory Load, CPU Jump, CPU Execute

3. Behavioral Patterns — How Objects Interact

Behavioral patterns are concerned with algorithms and the assignment of responsibilities between objects.

Observer

Defines a one-to-many dependency between objects.

Strategy

Defines a family of algorithms, encapsulates each one, and makes them interchangeable.

class Sorter {
    constructor(strategy) {
        this.strategy = strategy;
    }

    setStrategy(strategy) {
        this.strategy = strategy;
    }

    sort(data) {
        return this.strategy.sort(data);
    }
}

class BubbleSortStrategy {
    sort(data) {
        console.log("Bubble sort");
        return data.sort();
    }
}

class QuickSortStrategy {
    sort(data) {
        console.log("Quick sort");
        return data.sort();
    }
}

const data = [3, 1, 4, 1, 5, 9, 2, 6];

const sorter = new Sorter(new BubbleSortStrategy());
console.log(sorter.sort(data));

sorter.setStrategy(new QuickSortStrategy());
console.log(sorter.sort(data));

4. Worked Example — Three Patterns Doing Real Work Together

Patterns rarely show up one at a time. Below is a small shopping cart that uses three of them at once: Observer to announce changes, Module to keep the item list private, and Strategy to swap the pricing rule at runtime.

Read it once before running it. Every non-obvious line has a comment explaining what it does and why the pattern is there. Then press Run and check the output against the list at the bottom of the code.

// -- WORKED EXAMPLE - three patterns cooperating in one tiny app --
// Patterns are not academic. Here are three of them doing real work together.

// PATTERN 1: OBSERVER - a tiny event bus.
// Anything can subscribe; the bus never knows who is listening. That ignorance
// is the point: the cart can announce changes without importing the UI.
class EventBus {
  constructor() {
    this.handlers = {};                    // shape: { eventName: [fn, fn, ...] }
  }
  on(event, handler) {
    if (!this.handlers[event]) this.handlers[event] = [];   // first listener for this event
    this.handlers[event].push(handler);
  }
  emit(event, payload) {
    const list = this.handlers[event] || [];   // no listeners? then do nothing
    list.forEach(fn => fn(payload));           // fire in subscription order
  }
}

const bus = new EventBus();

// PATTERN 2: MODULE - an IIFE (a function that runs itself) that hides its state.
// 'items' is genuinely private: the only way in is the object that gets returned.
const Cart = ((bus) => {
  const items = [];                          // private - nothing outside can touch it

  const subtotal = () => items.reduce((sum, i) => sum + i.price, 0);

  const add = (name, price) => {
    items.push({ name: name, price: price });
    bus.emit("cart:changed", { count: items.length, subtotal: subtotal() });
  };

  return { add: add, subtotal: subtotal, size: () => items.length };
})(bus);

// PATTERN 3: STRATEGY - interchangeable algorithms with an identical shape.
// Every strategy is (number) => number, so swapping one for another is just
// an assignment. No growing if/else tree, no touching the cart code.
const discountStrategies = {
  none:    total => total,
  student: total => total * 0.9,                // 10% off
  voucher: total => Math.max(0, total - 15)     // flat 15 off, never below zero
};

let activeStrategy = discountStrategies.none;   // the currently chosen algorithm

// Two independent subscribers. Neither knows the other exists.
bus.on("cart:changed", (state) => {
  console.log("UI badge -> " + state.count + " item(s)");
});

bus.on("cart:changed", (state) => {
  const payable = activeStrategy(state.subtotal);
  console.log("Subtotal " + state.subtotal.toFixed(2) + " | to pay " + payable.toFixed(2));
});

// Now use it. Each add() fires BOTH subscribers, in the order they subscribed.
Cart.add("Keyboard", 45);
Cart.add("Mouse", 25);
Cart.add("Monitor", 180);

// Swap the pricing algorithm at runtime and re-announce the same state:
activeStrategy = discountStrategies.student;
bus.emit("cart:changed", { count: Cart.size(), subtotal: Cart.subtotal() });

activeStrategy = discountStrategies.voucher;
bus.emit("cart:changed", { count: Cart.size(), subtotal: Cart.subtotal() });

// The Module pattern's privacy holds: there is no public 'items'.
console.log("Direct access to items:", Cart.items);

// Expected output:
// UI badge -> 1 item(s)
// Subtotal 45.00 | to pay 45.00
// UI badge -> 2 item(s)
// Subtotal 70.00 | to pay 70.00
// UI badge -> 3 item(s)
// Subtotal 250.00 | to pay 250.00
// UI badge -> 3 item(s)
// Subtotal 250.00 | to pay 225.00
// UI badge -> 3 item(s)
// Subtotal 250.00 | to pay 235.00
// Direct access to items: undefined

Notice what you did not have to do: the cart was never edited when the discount rule changed, and neither subscriber was edited when the other was added. That is the whole return on using patterns — change lands in one place instead of five.

🏆 Mini-Challenge: Implement Command + Undo

One more behavioural pattern, and this time there are no blanks — only a brief, an outline and a test harness. Command turns "do a thing" into an object with an execute and an undo. Keep those objects in a list and undo falls out for free.

// 🎯 MINI-CHALLENGE: the Command pattern (with undo)
//
// Brief: in the Command pattern every change is an OBJECT that knows how to do
// itself and how to undo itself. Stack those objects up and you have undo -
// which is exactly how every Ctrl+Z you have ever pressed works.

// Given - do not change this line:
const doc = { text: "" };

// Outline - no logic filled in, that part is yours:
//
// 1. function appendCommand(str) -> returns an object with execute and undo
//      execute: add str to the end of doc.text
//      undo:    chop str back off the end
//               hint: doc.text.slice(0, doc.text.length - str.length)
//
// 2. function upperCommand() -> returns an object with execute and undo
//      execute: remember doc.text in a closure variable FIRST,
//               then uppercase doc.text
//      undo:    put the remembered text back
//
// 3. const history = []   plus two helpers:
//      function run(cmd)  -> cmd.execute(), then push cmd onto history
//      function undo()    -> pop the last command and call its undo()
//
// Until you have written run(), this throws
// "ReferenceError: run is not defined" - that is expected, not a bug.

// your code here


// --- test harness: do not change anything below this line ---
run(appendCommand("hello"));
run(appendCommand(" world"));
console.log(doc.text);

run(upperCommand());
console.log(doc.text);

undo();               // reverses the uppercase
console.log(doc.text);

undo();               // reverses the second append
console.log(doc.text);

// ✅ Expected output:
// hello world
// HELLO WORLD
// hello world
// hello

The tricky one is upperCommand. There is no way to reverse toUpperCase() by calculation — "HELLO" could have been "hello", "Hello" or "HeLLo" — so the command has to save the old text before it changes anything. Undo is usually a memory problem, not a maths problem.

What You've Mastered

You now have expert-level understanding of JavaScript design patterns:

These patterns are the foundation of professional JavaScript development. They're used in every major framework, game engine, and large-scale application. Master them, and you'll write cleaner, more maintainable, and more scalable code.

📋 Quick Reference — Design Patterns

PatternBest For
SingletonGlobal state (DB, Logger)
FactoryComplex object creation
ObserverEvent handling & subscriptions
ModuleEncapsulation & privacy
DecoratorExtending functionality

Lesson Complete — Design Patterns!

You now have a toolbox of proven architectural solutions. You don't just write code; you design systems.

Practice quiz

What are design patterns, according to the lesson?

  • Specific code you must copy exactly
  • Reusable, battle-tested solutions to common programming problems
  • A JavaScript framework
  • A type of data structure

Answer: Reusable, battle-tested solutions to common programming problems. Design patterns are reusable, battle-tested solutions to common problems, not specific code to copy but structured ideas you implement.

Into which three categories are the patterns grouped?

  • Creational, Structural, Behavioral
  • Public, Private, Static
  • Sync, Async, Parallel
  • Easy, Medium, Hard

Answer: Creational, Structural, Behavioral. The lesson groups patterns into Creational (building objects), Structural (composing objects), and Behavioral (how objects interact).

Which pattern ensures only one instance of a class exists?

  • Factory
  • Singleton
  • Builder
  • Decorator

Answer: Singleton. Singleton ensures only one instance exists; the lesson's Logger example shows logger1 === logger2 is true.

What does the Factory pattern do?

  • Adds responsibilities to an object
  • Creates objects without specifying the exact class to create
  • Notifies many observers
  • Encapsulates code into a single unit

Answer: Creates objects without specifying the exact class to create. The Factory pattern creates objects without specifying the exact class; the CarFactory.create('Tesla') example demonstrates this.

Which creational pattern separates the construction of a complex object from its representation, using chained methods?

  • Singleton
  • Builder
  • Module
  • Facade

Answer: Builder. The Builder pattern (QueryBuilder with .select().from().where().build()) separates complex construction from representation.

Which structural pattern encapsulates part of an app into a single unit using an IIFE?

  • Module
  • Decorator
  • Facade
  • Observer

Answer: Module. The Module pattern (the calculator IIFE returning add/subtract) encapsulates a part of the app into a single unit.

What does the Decorator pattern do?

  • Provides a simplified interface to a subsystem
  • Dynamically adds responsibilities to an object
  • Defines a one-to-many dependency
  • Ensures a single instance

Answer: Dynamically adds responsibilities to an object. Decorator dynamically adds responsibilities; MilkDecorator wraps Coffee to add cost and description.

In the lesson's example, what does milkCoffee.getCost() return when coffee costs 5 and milk adds 2?

  • 5
  • 2
  • 7
  • 10

Answer: 7. MilkDecorator.getCost() returns this.coffee.getCost() + 2, so 5 + 2 = 7.

Which structural pattern provides a simplified interface to a complex subsystem?

  • Observer
  • Strategy
  • Facade
  • Factory

Answer: Facade. The Facade pattern (ComputerFacade.start() hiding CPU, Memory, HardDrive) provides a simplified interface to a complex subsystem.

Which behavioral pattern defines a one-to-many dependency between objects?

  • Strategy
  • Observer
  • Module
  • Builder

Answer: Observer. The Observer pattern defines a one-to-many dependency: a Subject notifies all subscribed observers when something happens.

Continue this course