DEV Community

Cover image for Design Patterns in JavaScript
Pixel Mosaic
Pixel Mosaic

Posted on

Design Patterns in JavaScript

JavaScript gives us a lot of freedom—but with great flexibility comes the risk of messy code. That's where design patterns come in.

Design patterns are reusable solutions to common software design problems. They aren't libraries or frameworks; they're proven approaches that help you write code that's easier to maintain, extend, and understand.

Let's explore some of the most useful JavaScript design patterns with examples.

1. Module Pattern

The Module Pattern helps encapsulate data and expose only what's necessary.

const Counter = (() => {
  let count = 0;

  return {
    increment() {
      count++;
    },
    decrement() {
      count--;
    },
    getCount() {
      return count;
    }
  };
})();

Counter.increment();
console.log(Counter.getCount()); // 1
Enter fullscreen mode Exit fullscreen mode

Why use it?

  • Data privacy
  • Avoid global variables
  • Better organization

2. Singleton Pattern

A Singleton ensures only one instance of an object exists.

class Database {
  constructor() {
    if (Database.instance) {
      return Database.instance;
    }

    this.connection = "Connected";
    Database.instance = this;
  }
}

const db1 = new Database();
const db2 = new Database();

console.log(db1 === db2); // true
Enter fullscreen mode Exit fullscreen mode

Common Uses

  • Database connections
  • Configuration objects
  • Logging services

3. Factory Pattern

Instead of using new everywhere, a Factory creates objects based on input.

class Car {
  drive() {
    console.log("Driving a car");
  }
}

class Bike {
  ride() {
    console.log("Riding a bike");
  }
}

function vehicleFactory(type) {
  if (type === "car") return new Car();
  if (type === "bike") return new Bike();
}

const vehicle = vehicleFactory("car");
vehicle.drive();
Enter fullscreen mode Exit fullscreen mode

Benefits

  • Centralized object creation
  • Easy to add new object types
  • Reduces duplicated code

4. Observer Pattern

Used when multiple objects need updates after one object's state changes.

class Subject {
  constructor() {
    this.observers = [];
  }

  subscribe(observer) {
    this.observers.push(observer);
  }

  notify(data) {
    this.observers.forEach(observer => observer(data));
  }
}

const news = new Subject();

news.subscribe(message => {
  console.log("Subscriber 1:", message);
});

news.subscribe(message => {
  console.log("Subscriber 2:", message);
});

news.notify("New JavaScript update!");
Enter fullscreen mode Exit fullscreen mode

Real-world Examples

  • Event listeners
  • Redux
  • React state updates
  • Notifications

5. Prototype Pattern

Objects inherit behavior from other objects.

const person = {
  greet() {
    console.log(`Hello, I'm ${this.name}`);
  }
};

const john = Object.create(person);
john.name = "John";

john.greet();
Enter fullscreen mode Exit fullscreen mode

Benefits

  • Memory efficient
  • Shared methods
  • JavaScript's native inheritance model

6. Strategy Pattern

Choose an algorithm at runtime.

const paymentStrategies = {
  creditCard(amount) {
    console.log(`Paid $${amount} with Credit Card`);
  },

  paypal(amount) {
    console.log(`Paid $${amount} with PayPal`);
  }
};

function checkout(amount, strategy) {
  strategy(amount);
}

checkout(100, paymentStrategies.creditCard);
checkout(50, paymentStrategies.paypal);
Enter fullscreen mode Exit fullscreen mode

Use Cases

  • Payment gateways
  • Sorting algorithms
  • Authentication methods

7. Decorator Pattern

Adds new behavior without modifying existing code.

function coffee() {
  return "Coffee";
}

function withMilk(fn) {
  return () => fn() + " + Milk";
}

function withSugar(fn) {
  return () => fn() + " + Sugar";
}

const order = withSugar(withMilk(coffee));

console.log(order());
Enter fullscreen mode Exit fullscreen mode

Output:

Coffee + Milk + Sugar
Enter fullscreen mode Exit fullscreen mode

8. Adapter Pattern

Allows incompatible interfaces to work together.

class OldAPI {
  oldMethod() {
    return "Old API";
  }
}

class Adapter {
  constructor(api) {
    this.api = api;
  }

  newMethod() {
    return this.api.oldMethod();
  }
}

const adapter = new Adapter(new OldAPI());

console.log(adapter.newMethod());
Enter fullscreen mode Exit fullscreen mode

9. Command Pattern

Encapsulates requests as objects.

class Light {
  on() {
    console.log("Light ON");
  }
}

class LightCommand {
  constructor(light) {
    this.light = light;
  }

  execute() {
    this.light.on();
  }
}

const light = new Light();
const command = new LightCommand(light);

command.execute();
Enter fullscreen mode Exit fullscreen mode

10. MVC Pattern

Separates application logic into:

  • Model → Data
  • View → UI
  • Controller → Business logic

Many JavaScript frameworks (or their predecessors) have adopted versions of this architecture.


Which Pattern Should You Use?

Pattern Best For
Module Encapsulation
Singleton Single shared instance
Factory Object creation
Observer Event systems
Prototype Inheritance
Strategy Runtime algorithm selection
Decorator Extend functionality
Adapter Compatibility
Command Undo/redo, task queues
MVC Large applications

Final Thoughts

Design patterns are tools—not rules. Overusing them can make code more complex than necessary, but applying the right pattern to the right problem can improve readability, maintainability, and scalability.

As you build larger JavaScript applications, you'll likely encounter many of these patterns naturally in frameworks and libraries. Understanding the underlying ideas will help you recognize when a pattern fits your own codebase and when a simpler solution is enough.

Happy coding!

Top comments (0)