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
| Category | Purpose | Examples |
|---|---|---|
| Creational | How objects are created | Singleton, Factory, Builder |
| Structural | How objects are composed | Decorator, Facade, Adapter |
| Behavioral | How objects communicate | Observer, 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 state2. 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 54. 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 Headphones5. 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: 207. 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); // darkKey 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
