Design Patterns in JavaScript

Why should you care about Design Patterns in JavaScript?

Design patterns give you proven ways to solve common coding problems without reinventing everything. Learning them helps you write cleaner code, communicate better with teams, and understand framework architecture faster.

A design pattern is a common approach to solving a problem that developers often face. It describes the structure of a solution, not a finished piece of code to copy and paste.

Design patterns help you choose a clear structure, discuss design decisions with other developers, and change code without making every part depend on every other part. They are useful ideas, not rules that every project must follow.

The famous "Gang of Four" book introduced 23 patterns in three categories:

  • Creational Patterns - about how objects are created
  • Structural Patterns - about how objects are organised and composed
  • Behavioral Patterns - about how objects communicate with each other

You do not need to memorise all 23. JavaScript also has common patterns and idioms that are not from the original GoF list, such as Factory Functions, Constructor Functions, and the Module pattern. The language itself also provides features such as functions, classes, and ES modules. A feature is not automatically a design pattern.

Prerequisites: patterns become easier once you understand objects, functions, this, prototypes, and classes.

Good to know: React, Angular, Vue, and the Node.js runtime may use pattern-related ideas in different ways. Node.js is a JavaScript runtime, not a framework.

Creational patterns are all about how you create objects. Instead of using new SomeClass() everywhere, creational patterns give you smarter, more flexible ways to build things.

Factory Function Pattern

A factory function is a regular function that creates and returns an object. It is a common JavaScript pattern or idiom, not one of the original GoF 23 patterns.

Why use it? Because it hides the details of how an object is created. The code that uses the factory does not need to care about how it was built.

javascript

// Factory function - creates a service object
function createService(name, status) {
  return {
    name: name,
    status: status,
    describe() {
      console.log(`${this.name} is currently ${this.status}.`);
    }
  };
}

const api = createService("API Gateway", "running");
const db  = createService("Database",    "standby");

api.describe(); // API Gateway is currently running.
db.describe();  // Database is currently standby.

You can call createService() as many times as you want. Each call returns a fresh, independent object. No new keyword needed, no class required.

Constructor Pattern

Before ES6 classes existed, JavaScript developers used constructor functions to build objects. A constructor function is a regular function, but by convention it starts with a capital letter. You call it with the new keyword. Like factory functions, this is an important JavaScript pattern and idiom, but not one of the original GoF 23 patterns.

javascript

// Constructor function
function Car(brand, model) {
  this.brand = brand;
  this.model = model;
  this.describe = function() {
    console.log(`${this.brand} ${this.model}`);
  };
}

const car1 = new Car("Toyota", "Camry");
const car2 = new Car("Honda",  "Civic");

car1.describe(); // Toyota Camry
car2.describe(); // Honda Civic

When you use new, JavaScript automatically creates a new empty object, sets this to that object, and returns it. Today, most developers use ES6 class syntax instead - but understanding constructor functions helps you read older code.

Singleton Pattern

A Singleton provides one shared instance within a defined scope or module/runtime context. It does not necessarily mean one instance across every possible JavaScript environment.

Common use cases: a configuration manager, a database connection, a logging service.

javascript

// Singleton - only one instance ever exists
const AppConfig = (function() {
  let instance;

  function createInstance() {
    return {
      theme: "light",
      language: "en",
      version: "1.0.0"
    };
  }

  return {
    getInstance() {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    }
  };
})();

const config1 = AppConfig.getInstance();
const config2 = AppConfig.getInstance();

config1.theme = "dark";

console.log(config2.theme); // "dark" - same object!
console.log(config1 === config2); // true - same instance

Both config1 and config2 point to the same object in this module's scope. Changing one changes the other. This shared instance is the key idea of the Singleton pattern.

Be careful with Singletons: they can introduce hidden global state, increase tight coupling, and make testing harder. In modern JavaScript, ES modules often give you singleton-like shared module instances naturally.

Structural patterns are about how you organise and compose your code. They help you keep things tidy, hide complexity, and make different parts of your code work together without getting tangled.

Module Pattern

The Module pattern groups related code together and keeps some data private. An IIFE can create a private scope and return only the functions that other code should use.

This approach was common before ES6 modules. For new code, use native ES modules when they fit the project.

javascript

// Module Pattern using IIFE (Immediately Invoked Function Expression)
const Counter = (function() {
  // Private - not accessible from outside
  let count = 0;

  // Public - returned and accessible from outside
  return {
    increment() {
      count++;
    },
    decrement() {
      count--;
    },
    getCount() {
      return count;
    }
  };
})();

Counter.increment();
Counter.increment();
Counter.increment();
Counter.decrement();

console.log(Counter.getCount()); // 2
console.log(Counter.count);      // undefined - count is private!

The variable count is hidden inside the function. The outside world cannot touch it directly. You can only interact with it through the public methods (increment, decrement, getCount). This is called encapsulation.

Decorator Pattern

The Decorator pattern adds behavior to an existing object without changing its basic role. In this example, each decorator updates and returns the same coffee object, so features can be added one at a time.

javascript

// Base coffee object
function Coffee() {
  this.cost = function() { return 5; };
  this.description = function() { return "Coffee"; };
}

// Decorator: adds milk
function withMilk(coffee) {
  const previousCost = coffee.cost();
  const previousDescription = coffee.description();

  coffee.cost        = function() { return previousCost + 1; };
  coffee.description = function() { return previousDescription + ", Milk"; };

  return coffee;
}

// Decorator: adds sugar
function withSugar(coffee) {
  const previousCost = coffee.cost();
  const previousDescription = coffee.description();

  coffee.cost        = function() { return previousCost + 0.5; };
  coffee.description = function() { return previousDescription + ", Sugar"; };

  return coffee;
}

let myCoffee = new Coffee();
myCoffee = withMilk(myCoffee);
myCoffee = withSugar(myCoffee);

console.log(myCoffee.description()); // Coffee, Milk, Sugar
console.log(myCoffee.cost());        // 6.5

You start with a plain coffee and keep wrapping it with decorators. Each decorator updates the current coffee object and returns it, so you can stack as many decorators as you need.

Facade Pattern

The Facade pattern provides a simple interface to a complex system. Callers use the facade instead of coordinating each internal subsystem themselves.

javascript

// Complex subsystems
const CPU     = { start()    { console.log("CPU started");     } };
const Memory  = { load()     { console.log("Memory loaded");   } };
const Storage = { readData() { console.log("Storage read");    } };

// Facade - simple interface that hides all the complexity
const Computer = {
  start() {
    CPU.start();
    Memory.load();
    Storage.readData();
    console.log("Computer is ready!");
  }
};

// You only need to know one thing:
Computer.start();
// CPU started
// Memory loaded
// Storage read
// Computer is ready!

The caller uses Computer.start() without knowing about CPU, Memory, or Storage. The Facade hides those steps behind one entry point.

Two other structural patterns you may see are Adapter, which helps incompatible interfaces work together, and Prototype, which creates new objects by cloning existing ones. For Prototype fundamentals in JavaScript, see Prototypes and Inheritance.

Behavioral patterns are about how objects talk to each other. When one part of your code needs to communicate with another, behavioral patterns give you clean, maintainable ways to do it.

Observer Pattern

The Observer pattern allows multiple listeners to be notified when something happens. The listeners can react without the object that publishes the event knowing their details.

In code, one object, called the subject or publisher, maintains a list of observers. When something changes, it notifies them. An EventEmitter-style implementation is one practical form of this idea.

javascript

// Simple Observer / EventEmitter
class EventEmitter {
  constructor() {
    this.listeners = {};
  }

  on(event, callback) {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event].push(callback);
  }

  emit(event, data) {
    if (this.listeners[event]) {
      this.listeners[event].forEach(cb => cb(data));
    }
  }
}

const emitter = new EventEmitter();

// Subscribe - two observers
emitter.on("deploy", (version) => console.log(`Deploying ${version}...`));
emitter.on("deploy", (version) => console.log(`Logging deploy: ${version}`));

// Publish - both observers are notified automatically
emitter.emit("deploy", "v2.1.0");
// Deploying v2.1.0...
// Logging deploy: v2.1.0

The emitter does not need to know anything about its observers. It just fires the event and moves on. This loose connection between parts of your code makes your system flexible and easy to extend.

Strategy Pattern

The Strategy pattern lets you choose between different algorithms or behaviors without changing the code that uses them. Each strategy follows the same expected input and output.

javascript

// Different sort strategies
const bubbleSort = (arr) => {
  const a = [...arr];
  for (let i = 0; i < a.length; i++)
    for (let j = 0; j < a.length - i - 1; j++)
      if (a[j] > a[j+1]) [a[j], a[j+1]] = [a[j+1], a[j]];
  return a;
};

const nativeSort = (arr) => [...arr].sort((a, b) => a - b);

// Sorter uses whichever strategy you pass in
function Sorter(strategy) {
  this.sort = function(data) {
    return strategy(data);
  };
}

const numbers = [5, 1, 8, 2, 9, 3];

const sorter1 = new Sorter(bubbleSort);
console.log(sorter1.sort(numbers)); // [1, 2, 3, 5, 8, 9]

const sorter2 = new Sorter(nativeSort);
console.log(sorter2.sort(numbers)); // [1, 2, 3, 5, 8, 9]

The Sorter does not care which sorting algorithm you use. You pass in the strategy and it does the work. This makes it easy to swap algorithms without touching the code that calls the sorter.

Command Pattern

The Command pattern turns actions into objects. Instead of calling a function directly, you wrap the call inside a command object. This makes it easy to queue actions and keep a history of what was executed.

javascript

// Command objects
const light = { on: false };

const turnOnCommand  = { execute: () => { light.on = true;  console.log("Light ON");  } };
const turnOffCommand = { execute: () => { light.on = false; console.log("Light OFF"); } };

// Invoker - processes the command queue
class RemoteControl {
  constructor() { this.history = []; }

  press(command) {
    command.execute();
    this.history.push(command);
  }
}

const remote = new RemoteControl();
remote.press(turnOnCommand);   // Light ON
remote.press(turnOffCommand);  // Light OFF
remote.press(turnOnCommand);   // Light ON

console.log("Commands run:", remote.history.length); // 3

Each command is a self-contained object with an execute() method. The remote control just calls execute() - it does not know or care what the command actually does. The history array in this example records which commands were run.

Another behavioral pattern you may meet is State, where an object changes its behavior when its internal state changes.

  • Design patterns are reusable approaches to common problems. They describe a solution without being copy-paste code.
  • Creational patterns control how objects are made. Factory Functions hide construction details. Constructor Functions use new to build objects. Singleton provides one shared instance within a defined scope.
  • Structural patterns organise how code is put together. Module Pattern keeps things private and grouped. Decorator layers behaviour onto an object. Facade simplifies complex systems with a clean interface. Adapter helps mismatched interfaces work together.
  • Behavioral patterns define how parts of your code communicate. Observer notifies many subscribers when something changes. Strategy swaps algorithms at runtime. Command turns actions into objects and can keep execution history. State changes behavior based on current state.
  • You do not need to memorise all 23 patterns. Focus on the problem each pattern solves and when it is useful.
  • Frameworks and runtimes often use ideas related to patterns. React, Angular, Vue, and the Node.js runtime may apply them in different ways.

Design patterns are tools, not rules. Use them when they make a design easier to understand, change, or maintain.

Compare each pattern by the design problem it addresses, how it works, and the trade-off to remember.
Pattern Category Core idea Use it when... Watch out for...
Factory Function Creational A function returns a fresh object. You want simple object creation without new. Repeated methods can use more memory unless shared deliberately.
Constructor Creational new creates an object and sets this. You are maintaining older constructor-based code or need prototype methods. Forgetting new can cause confusing behavior.
Singleton Creational One shared instance within a defined scope. One configuration or service must be shared. Hidden global state makes tests and dependencies harder to control.
Module Structural Public functions expose a small API around private state. You want boundaries and encapsulation. Old IIFE modules can be harder to navigate than ES modules.
Decorator Structural Wrap an object to add behavior. Features should be combined without editing the base object. Many wrappers can make the final behavior difficult to trace.
Facade Structural One simple interface hides several subsystems. Callers should not depend on complex internal steps. The facade can become hard to maintain if it takes on too many responsibilities.
Observer Behavioral Subscribers react when a publisher changes. One event should update many independent listeners. Forgotten subscriptions can cause memory leaks or duplicate reactions.
Strategy Behavioral Swap an algorithm behind the same interface. Several interchangeable rules or algorithms are possible. Do not create strategies for logic that is only used once.
Command Behavioral Represent an action as an object with an execution method. You need queues, undo, logging, or action history. Wrapping every tiny function adds unnecessary ceremony.
What's next? Continue with Performance Optimization, then review OOP and Classes and Prototypes and Inheritance.

Practical rule: start with simple code. Introduce a pattern when it makes the code easier to understand, change, or maintain.

Reviewed by

SimplyJavaScript Editorial Team

Technical editors and JavaScript educators with hands-on experience building frontend projects, writing learning material, and reviewing tutorials for clarity, accuracy, and beginner-friendly guidance.