Design Patterns
Proven solutions to common problems. Patterns that professional developers use daily.
Singleton
One instance, shared everywhere. Global state done right.
// Singleton — only ONE instance ever exists:
class Database {
static #instance = null;
constructor() {
if (Database.#instance) {
return Database.#instance; // return existing
}
this.connection = createConnection();
Database.#instance = this;
}
query(sql) {
return this.connection.execute(sql);
}
}
const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // true — same instance!
// Module pattern (simpler in ES modules):
// Each module is already a singleton!
// config.js:
const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
};
export default config;
// Every import gets the SAME object.
// Practical use — app-wide store:
let state = { user: null, theme: "dark" };
export function getState() { return state; }
export function setState(patch) { state = { ...state, ...patch }; }Observer
When something changes, notify everyone who cares.
// Observer — one-to-many dependency:
// When subject changes, all observers are notified.
class EventEmitter {
#listeners = new Map();
on(event, callback) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, []);
}
this.#listeners.get(event).push(callback);
// Return unsubscribe function:
return () => this.off(event, callback);
}
off(event, callback) {
const list = this.#listeners.get(event);
if (list) {
this.#listeners.set(event, list.filter(cb => cb !== callback));
}
}
emit(event, data) {
const list = this.#listeners.get(event) || [];
list.forEach(callback => callback(data));
}
}
// Usage:
const emitter = new EventEmitter();
const unsub = emitter.on("userLogin", (user) => {
console.log(`${user.name} logged in!`);
});
emitter.emit("userLogin", { name: "Alice" });
// "Alice logged in!"
unsub(); // stop listeningFactory
Create objects without specifying exact classes.
// Factory — creation logic in one place:
function createNotification(type, message) {
const base = { id: crypto.randomUUID(), message, timestamp: Date.now() };
switch (type) {
case "success":
return { ...base, icon: "✓", color: "green", duration: 3000 };
case "error":
return { ...base, icon: "✕", color: "red", duration: 0 }; // stays
case "warning":
return { ...base, icon: "⚠", color: "yellow", duration: 5000 };
default:
return { ...base, icon: "ℹ", color: "blue", duration: 4000 };
}
}
// Caller doesn't know internal structure:
const n1 = createNotification("success", "Saved!");
const n2 = createNotification("error", "Connection failed");
// Factory with registration (extensible):
const validators = {};
function registerValidator(type, fn) {
validators[type] = fn;
}
function validate(type, value) {
const validator = validators[type];
if (!validator) throw new Error(`Unknown type: ${type}`);
return validator(value);
}
registerValidator("email", v => v.includes("@"));
registerValidator("phone", v => /^\d{10}$/.test(v));
validate("email", "a@b.com"); // trueStrategy
Swap algorithms at runtime without changing calling code.
// Strategy — interchangeable behaviors:
const sortStrategies = {
byName: (a, b) => a.name.localeCompare(b.name),
byDate: (a, b) => new Date(b.date) - new Date(a.date),
byPrice: (a, b) => a.price - b.price,
byRating: (a, b) => b.rating - a.rating,
};
function sortProducts(products, strategy = "byName") {
return [...products].sort(sortStrategies[strategy]);
}
// Swap sorting without changing function:
sortProducts(items, "byPrice");
sortProducts(items, "byRating");
// Auth strategy:
const authStrategies = {
jwt: {
authenticate(req) { return verifyJWT(req.headers.authorization); },
},
apiKey: {
authenticate(req) { return checkApiKey(req.headers["x-api-key"]); },
},
session: {
authenticate(req) { return getSession(req.cookies.sid); },
},
};
function authenticate(req, strategy) {
return authStrategies[strategy].authenticate(req);
}
// Easy to add new strategies without modifying existing code!Pub/Sub
Decouple publishers from subscribers completely.
// Pub/Sub — publishers don't know about subscribers:
// Unlike Observer, there's a message broker in between.
class MessageBus {
#channels = new Map();
subscribe(channel, handler) {
if (!this.#channels.has(channel)) {
this.#channels.set(channel, new Set());
}
this.#channels.get(channel).add(handler);
return () => this.#channels.get(channel).delete(handler);
}
publish(channel, data) {
const handlers = this.#channels.get(channel);
if (handlers) handlers.forEach(h => h(data));
}
}
const bus = new MessageBus();
// Components subscribe independently:
bus.subscribe("cart:updated", (cart) => {
updateCartIcon(cart.count);
});
bus.subscribe("cart:updated", (cart) => {
updateTotal(cart.total);
});
// Any component can publish — no direct coupling:
function addToCart(product) {
cart.items.push(product);
bus.publish("cart:updated", {
count: cart.items.length,
total: cart.items.reduce((s, i) => s + i.price, 0),
});
}
// Real-world: Redux, Zustand, and React Context
// all use variations of Pub/Sub under the hood.FAQ
Common questions about patterns.