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 listening

Factory

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"); // true

Strategy

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.