Design Patterns Overview
Design patterns are reusable solutions to common programming problems. They're not code — they're templates. Learning them gives you vocabulary and proven approaches to structure your code.
Singleton Pattern
Ensures one instance exists globally. Perfect for database connections, configuration, logging.
class Database {
static #instance = null;
#connection;
constructor(url) {
if (Database.#instance) return Database.#instance;
this.#connection = url;
Database.#instance = this;
}
static getInstance(url) { return new Database(url); }
query(sql) { return "Querying: " + sql; }
}
const db1 = Database.getInstance("postgres://localhost/app");
const db2 = Database.getInstance("postgres://localhost/app");
console.log(db1 === db2); // true — same instance!
// Module-based singleton (simpler):
// db.js
let connection = null;
export function getDb() {
if (!connection) connection = createConnection();
return connection;
}
Factory Pattern
// Create objects without specifying exact class:
function createNotification(type, message, options = {}) {
const base = { message, createdAt: new Date(), ...options };
switch (type) {
case "email": return { ...base, type: "email", send: sendEmail };
case "sms": return { ...base, type: "sms", send: sendSMS };
case "push": return { ...base, type: "push", send: sendPush };
default: throw new Error("Unknown type: " + type);
}
}
const notif = createNotification("email", "Welcome!", { to: "alice@example.com" });
notif.send();
Observer Pattern
class EventEmitter {
#events = {};
on(event, fn) {
(this.#events[event] ??= []).push(fn);
return () => this.off(event, fn); // returns unsubscribe
}
once(event, fn) {
const wrapper = (...args) => { fn(...args); this.off(event, wrapper); };
return this.on(event, wrapper);
}
off(event, fn) {
this.#events[event] = (this.#events[event] || []).filter(f => f !== fn);
}
emit(event, ...args) {
(this.#events[event] || []).forEach(fn => fn(...args));
}
}
const emitter = new EventEmitter();
const unsub = emitter.on("data", (d) => console.log("Got:", d));
emitter.emit("data", { users: [] });
unsub(); // stop listening
⚡ Key Takeaways
- Singleton: one shared instance — modules are singletons by default
- Factory: create objects without exposing creation logic
- Observer: decouple producers from consumers via events
- Learn the vocabulary — patterns communicate intent between developers
- Don't over-engineer — use patterns when they solve a real problem
🎯 Practice Exercises
EXERCISE 1
Build a simple reactive state store using Observer pattern: state.set(key, value) notifies all subscribers. Multiple components can subscribe to specific keys.