JavaScript Design Patterns

Design patterns are reusable solutions to commonly occurring problems in software development. They are not specific code snippets — they are blueprints that guide how to structure code to solve a problem effectively.

Understanding design patterns elevates code quality, improves maintainability, and makes it easier to communicate ideas within a team.

Why Design Patterns?

  • Provide proven, tested solutions to recurring problems
  • Make code more readable and understandable
  • Improve communication — "I used a Factory pattern" is clearer than a long explanation
  • Reduce the risk of introducing bugs with untested approaches

Categories of Design Patterns

CategoryPurposeExamples
CreationalHow objects are createdSingleton, Factory, Builder
StructuralHow objects are composedDecorator, Facade, Adapter
BehavioralHow objects communicateObserver, Strategy, Command

1. Singleton Pattern

The Singleton pattern ensures that a class has only one instance, and provides a single global access point to it. Useful for shared resources like configuration or a database connection.

class AppConfig {
  constructor() {
    if (AppConfig.instance) {
      return AppConfig.instance;  // Return existing instance
    }
    this.theme    = "light";
    this.language = "English";
    AppConfig.instance = this;
  }
}

let config1 = new AppConfig();
let config2 = new AppConfig();

config1.theme = "dark";

console.log(config1 === config2);  // true — same object
console.log(config2.theme);        // "dark" — both share the same state

2. Factory Pattern

The Factory pattern provides a function or method to create objects without specifying the exact class. The factory decides which type to create based on input.

class Dog {
  speak() { console.log("Woof!"); }
}

class Cat {
  speak() { console.log("Meow!"); }
}

class Bird {
  speak() { console.log("Tweet!"); }
}

function animalFactory(type) {
  const animals = { dog: Dog, cat: Cat, bird: Bird };
  const Animal  = animals[type.toLowerCase()];
  if (!Animal) throw new Error(`Unknown animal type: ${type}`);
  return new Animal();
}

let pet1 = animalFactory("dog");
let pet2 = animalFactory("cat");

pet1.speak(); // Woof!
pet2.speak(); // Meow!

3. Builder Pattern

The Builder pattern constructs complex objects step by step. It separates the construction of an object from its representation.

class QueryBuilder {
  constructor(table) {
    this.table      = table;
    this.conditions = [];
    this.columns    = "*";
    this.limitVal   = null;
  }

  select(...cols) {
    this.columns = cols.join(", ");
    return this;  // Return this for chaining
  }

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

  limit(n) {
    this.limitVal = n;
    return this;
  }

  build() {
    let query = `SELECT ${this.columns} FROM ${this.table}`;
    if (this.conditions.length) {
      query += ` WHERE ${this.conditions.join(" AND ")}`;
    }
    if (this.limitVal) {
      query += ` LIMIT ${this.limitVal}`;
    }
    return query;
  }
}

let query = new QueryBuilder("students")
  .select("name", "score")
  .where("score > 80")
  .where("grade = 10")
  .limit(5)
  .build();

console.log(query);
// SELECT name, score FROM students WHERE score > 80 AND grade = 10 LIMIT 5

4. Observer Pattern

The Observer pattern defines a one-to-many dependency. When one object (the subject) changes state, all dependent objects (observers) are notified automatically. This is the pattern behind events and reactive state management (Redux, Vue.js).

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

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

  off(event, callback) {
    if (this.listeners[event]) {
      this.listeners[event] = this.listeners[event].filter(fn => fn !== callback);
    }
  }

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

const store = new EventEmitter();

function onPurchase(item) {
  console.log(`Order placed for: ${item}`);
}

function sendEmail(item) {
  console.log(`Confirmation email sent for: ${item}`);
}

store.on("purchase", onPurchase);
store.on("purchase", sendEmail);

store.emit("purchase", "Wireless Headphones");
// Order placed for: Wireless Headphones
// Confirmation email sent for: Wireless Headphones

5. Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. The algorithm can be switched at runtime without changing the code that uses it.

// Different sorting strategies
const bubbleSort = (arr) => {
  console.log("Using Bubble Sort");
  return [...arr].sort((a, b) => a - b);
};

const quickSort = (arr) => {
  console.log("Using Quick Sort");
  return [...arr].sort((a, b) => a - b);
};

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

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

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

let sorter = new Sorter(bubbleSort);
console.log(sorter.sort([5, 2, 8, 1]));   // Using Bubble Sort → [1, 2, 5, 8]

sorter.setStrategy(quickSort);
console.log(sorter.sort([5, 2, 8, 1]));   // Using Quick Sort → [1, 2, 5, 8]

6. Decorator Pattern

The Decorator pattern adds new behavior to an existing object dynamically without modifying its structure. It wraps the original and extends it.

function withLogging(fn) {
  return function(...args) {
    console.log(`Calling ${fn.name} with args:`, args);
    let result = fn(...args);
    console.log(`Result:`, result);
    return result;
  };
}

function multiply(a, b) {
  return a * b;
}

let loggedMultiply = withLogging(multiply);

loggedMultiply(4, 5);
// Calling multiply with args: [4, 5]
// Result: 20

7. Module Pattern

The Module pattern uses closures to create private and public sections — encapsulating state and exposing only what is needed.

const Counter = (function() {
  let count = 0;  // Private

  return {
    increment() { count++;  console.log("Count:", count); },
    decrement() { count--;  console.log("Count:", count); },
    reset()     { count = 0; console.log("Reset to 0"); },
    getCount()  { return count; }
  };
})();

Counter.increment(); // Count: 1
Counter.increment(); // Count: 2
Counter.decrement(); // Count: 1
console.log(Counter.getCount()); // 1
// console.log(Counter.count);   // undefined — private!

8. Facade Pattern

The Facade pattern provides a simplified interface to a complex system. It hides the complexity and exposes only what the caller needs.

// Complex subsystems
class AuthService {
  login(user, pass) { return user === "admin" && pass === "1234"; }
}

class ProfileService {
  load(userId) { return { id: userId, name: "Ananya", role: "admin" }; }
}

class SettingsService {
  load(userId) { return { theme: "dark", notifications: true }; }
}

// Facade — simple interface for the above complexity
class AppFacade {
  constructor() {
    this.auth     = new AuthService();
    this.profile  = new ProfileService();
    this.settings = new SettingsService();
  }

  startSession(username, password) {
    if (!this.auth.login(username, password)) {
      return { success: false, message: "Invalid credentials" };
    }
    let userId   = 101;
    let profile  = this.profile.load(userId);
    let settings = this.settings.load(userId);
    return { success: true, profile, settings };
  }
}

let app    = new AppFacade();
let result = app.startSession("admin", "1234");
console.log(result.profile.name);          // Ananya
console.log(result.settings.theme);        // dark

Key Points to Remember

  • Design patterns are reusable solutions — not specific code, but blueprints
  • Singleton ensures only one instance of a class exists globally
  • Factory creates objects without specifying the exact class at call time
  • Builder constructs complex objects step by step using method chaining
  • Observer allows objects to subscribe to and react to events from another object
  • Strategy makes algorithms interchangeable at runtime
  • Decorator adds behavior to functions or objects without modifying originals
  • Module uses closures to create private state with a public API
  • Facade provides a simple interface to a complex system

Leave a Comment

Your email address will not be published. Required fields are marked *