JavaScript Design Patterns
Design patterns are not just templates—they are professional solutions to recurring architectural challenges. Master the patterns that separate spaghetti code from scalable, enterprise-grade JavaScript.
Creational Patterns: Object Lifecycle
Creational patterns provide mechanisms for object creation that increase flexibility and reuse of existing code. In modern JavaScript, the **Singleton** is often implemented via ES Modules, while the **Factory** pattern remains essential for decoupling object usage from its construction logic.
// 1. Singleton (Modern ESM Approach)
// The module system itself caches exports, making this the cleanest Singleton.
class DatabaseService {
constructor() {
this.connection = null;
}
connect() {
if (!this.connection) {
this.connection = "Connected to DB";
console.log("New connection established");
}
return this.connection;
}
}
export const db = new DatabaseService(); // Single instance shared across app
// 2. Factory Pattern (Decoupling Creation)
class NotificationFactory {
static create(type) {
switch (type) {
case 'email': return new EmailProvider();
case 'sms': return new SMSProvider();
default: throw new Error("Unknown provider");
}
}
}Behavioral Patterns: Communication
Behavioral patterns focus on how objects communicate and interact. The **Observer** pattern (or EventEmitter) forms the backbone of event-driven systems like Node.js and React, while the **Strategy** pattern allows you to swap algorithms at runtime without modifying the calling code.
// 1. Observer Pattern (The Event Bus)
class EventEmitter {
constructor() { this.events = {}; }
on(event, listener) {
(this.events[event] || (this.events[event] = [])).push(listener);
}
emit(event, ...data) {
(this.events[event] || []).forEach(fn => fn(...data));
}
}
// 2. Strategy Pattern (Interchangeable Logic)
const paymentStrategies = {
stripe: (amt) => console.log(`Processing ${amt} via Stripe`),
paypal: (amt) => console.log(`Processing ${amt} via PayPal`)
};
function checkout(amount, strategy) {
paymentStrategies[strategy](amount);
}Structural Patterns: Relationship Management
Structural patterns explain how to assemble objects and classes into larger structures while keeping them flexible and efficient. The **Proxy** pattern is a modern favorite (and a core part of Vue's reactivity system), allowing you to intercept and validate operations on an object before they reach the target.
// Proxy Pattern (Validation/Logging Layer)
const user = { name: "Neo", role: "user" };
const adminProxy = new Proxy(user, {
set(target, prop, value) {
if (prop === "role" && value === "admin") {
console.error("Unauthorized: Role escalation blocked.");
return false;
}
target[prop] = value;
return true;
}
});
adminProxy.role = "admin"; // Fails: Unauthorized escalation.Technical Insight: Pattern Over-Engineering
Senior engineers understand that patterns come with a "Complexity Tax." While the Factory pattern adds flexibility, it also adds an extra layer of abstraction. Always apply the **YAGNI** (You Ain't Gonna Need It) principle: start with simple objects and refactor into patterns only when the need for scalability or decoupling becomes evident.
Design Pattern Best Practices:
- ✅ **Singleton:** Use ES Modules for clean, cached single instances.
- ✅ **Factory:** Use when the exact type of object can only be determined at runtime.
- ✅ **Observer:** Ideal for decoupling UI components from business logic.
- ✅ **Proxy:** Use for logging, validation, and creating reactive data structures.
- ⌠**Anti-Pattern:** Avoid nesting patterns too deeply, as it obscures the data flow.