Skip to content

Design Patterns in System Design — Full Reference

How to use this page. Think of this as the index for the entire 09-design-patterns/ module. It catalogs every pattern you will meet while designing large-scale systems, shows the smallest possible Node.js sketch of each, and links out to the deep-dive pages in this folder for the full treatment.


1. The Mental Model — From Code Object to Data Center

Design patterns and system design are the same idea at different scales:

  • A GoF pattern solves a recurring problem inside one process (objects talking to objects).
  • A distributed pattern solves the same kind of recurring problem across processes and machines (services talking to services).

Once you internalise the table below, almost every system-design topic becomes familiar — it is just the network-aware version of a pattern you already know.

LayerWhat you manipulateWhy it matters at scale
🧱 Code objectClass instances, function callsA bug here affects one process.
🧩 GoF patternObjects in memory (Singleton, Factory, Observer, …)Reusable solution to local complexity.
🏢 ServiceAPIs, threads, queues owned by one teamA bug here affects one service / one team.
🌐 Distributed patternServices across machines (Circuit Breaker, Saga, CQRS, …)A bug here can cascade across the company.
🏭 Data centerRegions, replication, shardingA bug here affects availability globally.

Layered View


2. GoF Patterns at a Glance

The original Gang of Four (Gamma, Helm, Johnson, Vlissides) catalogued 23 patterns in three groups. Below is the subset you will keep meeting in system-design interviews and code reviews. Each entry follows the same shape:

Intent → When to use → System-design counterpart → Tiny Node.js sketch.


2.1 Creational Patterns (How objects are born)

A. Singleton

  • Intent: Guarantee exactly one instance of a thing for the whole process.
  • When to use: A resource that is expensive to create and safe to share (DB pool, Redis client, feature-flag client).
  • Distributed counterpart: Connection pool / shared cache cluster (one logical instance behind a load balancer).
javascript
// dbPool.js — Node.js modules are cached, so the instance is shared naturally.
const { Pool } = require("pg");

class DbPool {
  static #instance;
  static get() {
    if (!DbPool.#instance) {
      DbPool.#instance = new Pool({
        max: 20,
        connectionString: process.env.DATABASE_URL,
      });
    }
    return DbPool.#instance;
  }
}

module.exports = DbPool;

B. Factory

  • Intent: Hide the which-class-do-I-instantiate decision behind one function.
  • When to use: You support multiple back-ends (Stripe / Razorpay, Email / SMS / Push) and the rest of the code should not care.
  • Distributed counterpart: Strategy at the service level + API Gateway routing rules.
javascript
class EmailSender {
  send(m) {
    console.log("email:", m);
  }
}
class SmsSender {
  send(m) {
    console.log("sms:", m);
  }
}
class PushSender {
  send(m) {
    console.log("push:", m);
  }
}

class NotifierFactory {
  static create(channel) {
    return {
      email: new EmailSender(),
      sms: new SmsSender(),
      push: new PushSender(),
    }[channel];
  }
}

NotifierFactory.create(process.env.CHANNEL).send("Welcome!");

C. Builder

  • Intent: Step-by-step assembly of a complex object without a giant constructor.
  • When to use: Building a complex SQL query, an Elasticsearch DSL, or a request DTO with many optional fields.
  • Distributed counterpart: Pipeline / Chain-of-Responsibility assembled in config.
javascript
class QueryBuilder {
  constructor() {
    this.q = { fields: [], where: [], limit: 0 };
  }
  select(fields) {
    this.q.fields.push(...fields);
    return this;
  }
  where(c) {
    this.q.where.push(c);
    return this;
  }
  setLimit(n) {
    this.q.limit = n;
    return this;
  }
  toString() {
    return `SELECT ${this.q.fields.join(", ")} FROM users WHERE ${this.q.where.join(" AND ")} LIMIT ${this.q.limit}`;
  }
}

new QueryBuilder()
  .select("id", "email")
  .where("active = true")
  .setLimit(10)
  .toString();

D. Prototype

  • Intent: Clone an existing object instead of building one from scratch.
  • When to use: Caching heavy parsed templates, response shapes, or pre-warmed config objects.
  • Distributed counterpart: Read-through cache that returns a copy of the cached value (so callers can mutate it freely).
javascript
const baseConfig = Object.freeze({ region: "ap-south-1", retries: 3 });
const clone = (overrides) => ({ ...structuredClone(baseConfig), ...overrides });
clone({ retries: 5 }); // → { region: "ap-south-1", retries: 5 }

2.2 Structural Patterns (How objects are composed)

A. Adapter

  • Intent: Translate one interface into another so incompatible pieces fit.
  • When to use: Wrapping a legacy SOAP service behind a JSON REST endpoint, or normalising a third-party API.
  • Distributed counterpart: Anti-Corruption Layer (DDD) at a service boundary.
javascript
// Legacy GPS service returns minutes, our app needs seconds.
class LegacyGps {
  minutesTo(coord) {
    /* … */ return 8;
  }
}
class GpsAdapter {
  constructor(gps) {
    this.gps = gps;
  }
  secondsTo(coord) {
    return this.gps.minutesTo(coord) * 60;
  }
}

B. Decorator

  • Intent: Wrap an object to add behaviour without changing its class.
  • When to use: Express/Koa middleware (auth → logging → rateLimit → handler), retries, metrics.
  • Distributed counterpart: Service mesh sidecar (Envoy/Istio decorators like auth, mTLS, retries).
javascript
const withLogging = (fn) => async (req, res, next) => {
  console.log(req.method, req.url);
  return fn(req, res, next);
};
const withAuth = (fn) => async (req, res, next) => {
  if (!req.headers.authorization) return res.status(401).end();
  return fn(req, res, next);
};

C. Facade

  • Intent: One simple entry point that hides a complex subsystem.
  • When to use: A BFF (Backend-for-Frontend) that aggregates profile + orders + recommendations.
  • Distributed counterpart: API Gateway / BFF — the canonical Facade at company scale.

D. Proxy

  • Intent: A stand-in that controls access to the real object.
  • When to use: Lazy loading, access control, CDN (cache proxy), API gateway (rate-limit proxy), circuit breaker wrapper.
  • Distributed counterpart: Reverse proxy / CDN / Circuit Breaker wrapper — same idea, network-aware.

E. Composite

  • Intent: Treat a group of objects the same way as a single object (tree of files, menu of menus).
  • When to use: Building hierarchical permissions, org charts, recursive part-explosion in e-commerce.
  • Distributed counterpart: Tree-sharded data + recursive RPC traversal.
javascript
class MenuItem {
  constructor(name) {
    this.name = name;
    this.children = [];
  }
  add(item) {
    this.children.push(item);
  }
  print(indent = "") {
    console.log(indent + this.name);
    this.children.forEach((c) => c.print(indent + "  "));
  }
}

2.3 Behavioral Patterns (How objects talk and make decisions)

A. Observer

  • Intent: Subject maintains a list of dependents and notifies them on state change.
  • When to use: React/Angular reactivity, EventEmitter, UI state stores.
  • Distributed counterpart: Pub/Sub over a Message Broker (Redis, RabbitMQ, Kafka, NATS).
  • 📖 Deep dive: Connecting System Design with Design Patterns

B. Strategy

  • Intent: Pick an algorithm at runtime without if/else soup.
  • When to use: Cache eviction (LRU / LFU / FIFO), routing policies, pricing rules.
  • Distributed counterpart: Pluggable routing/replication policy (consistent hash vs round-robin).
javascript
const routing = {
  roundRobin: (servers, key) => servers[Math.abs(hash(key)) % servers.length],
  consistentHash: (servers, key) => servers[hash(key) % servers.length],
};
routing[process.env.ROUTER || "roundRobin"](serverList, userId);

C. State

  • Intent: Behaviour changes with internal state; the object looks like a different class.
  • When to use: TCP connection states, HTTP request lifecycle, Circuit Breaker states.
  • Distributed counterpart: Circuit Breaker state machine (CLOSED → OPEN → HALF-OPEN).
  • 📖 Deep dive: Circuit Breakers

D. Chain of Responsibility

  • Intent: Pass a request along a chain until something handles it.
  • When to use: Express middleware, validation pipelines, log levels (info → warn → drop).
  • Distributed counterpart: API Gateway filter chain / Sidecar pipeline / Saga step links.

E. Template Method

  • Intent: Base class defines the skeleton; subclasses fill in the steps.
  • When to use: Job runners, test fixtures, ETL pipelines.
  • Distributed counterpart: Workflow engines (Airflow, Temporal, Step Functions) that fix the skeleton and let you plug in steps.
javascript
class JobRunner {
  run() {
    this.setup();
    this.execute();
    this.teardown();
  }
}
class EmailJob extends JobRunner {
  setup() {
    /*load template*/
  }
  execute() {
    /*send*/
  }
  teardown() {
    /*release*/
  }
}

F. Command / Mediator / Iterator / Memento / Visitor / Interpreter

Less common in system design, but still useful:

PatternIntentSystem-design echo
CommandEncapsulate a request as an objectCommand queue / CQRS command side
MediatorCentral hub coordinates peersService registry / Orchestrator in Saga
IteratorStream items without exposing internalsCursor-based pagination / Kafka consumer iterator
MementoSave/restore snapshotsSnapshotting (DB, Redis RDB)
VisitorAdd operations without changing the classLinting / static analysis passes
InterpreterGrammar for a custom languageRule engines / feature flags DSL

3. Distributed / Architectural Patterns

These are the patterns you must know for any system-design interview. Most are treated in their own deep-dive pages — click through for the long-form version.

#PatternOne-line summaryDeep dive
1Circuit BreakerStop calling a downstream that is failing, recover slowly.circuit-breakers
2SagaDistributed transaction built from local steps + compensations.saga-pattern
3Event SourcingPersist every change as an immutable event; derive state on read.event-sourcing
4CQRSSeparate write and read models for the same domain.§ 3.1 below
5BulkheadIsolate failures into separate resource pools.§ 3.2 below
6Retry + Exponential BackoffRetry with growing + jittered delay.exponential-backoff
7Idempotency KeysDeduplicate retries of mutating operations.idempotency-keys
8OutboxAtomically write a DB change and a message to publish.§ 3.3 below
9SidecarRun a helper container/process next to the main one.§ 3.4 below
10AmbassadorOut-of-process proxy that handles cross-cutting network concerns.§ 3.5 below
11ShardingSplit one huge dataset across many smaller ones.§ 3.6 below
12Leader ElectionElect exactly one coordinator among peers.§ 3.7 below

3.1 CQRS — Command Query Responsibility Segregation

Split the write path from the read path. Writes go through a normalised model and produce events; reads are served from denormalised projections tuned for the query.

javascript
// Command side
async function createOrder(cmd) {
  await writeDb.orders.insert({
    id: cmd.id,
    status: "PENDING",
    total: cmd.total,
  });
  await bus.publish("OrderCreated", { id: cmd.id, total: cmd.total });
}

// Projection (Read side)
bus.subscribe("OrderCreated", async (e) => {
  await readDb.orderSummary.upsert({ id: e.id, total: e.total, open: 1 });
});

When to use: the read workload is very different from the write workload (analytics vs. OLTP), you need scalable reads, or you want independent evolution of each side. Watch out: eventual consistency on the read side; you must design queries to tolerate staleness.


3.2 Bulkhead

Partition resources into isolated pools so one tenant's flood cannot sink the whole ship.

javascript
// Conceptual limits per tenant
const LIMITS = {
  premium: { concurrency: 100, qps: 1000 },
  free: { concurrency: 20, qps: 100 },
};

Real-world example: Hystrix thread pools, Istio connection pools, Kubernetes namespaces with quotas.


3.3 Outbox Pattern

Guarantee atomic "change the DB and tell someone about it" without a 2-phase commit.

sql
-- Inside one transaction
INSERT INTO orders (id, total) VALUES (1, 500);
INSERT INTO outbox (topic, payload) VALUES ('OrderCreated', '{"id":1,"total":500}');
COMMIT;
-- A poller/CDC streams the outbox table onto Kafka.

When to use: you must publish a message and update your DB exactly once across both. Pair with CDC (Debezium) for the streaming tail.


3.4 Sidecar

Deploy a helper process alongside the main app to absorb cross-cutting concerns (mTLS, retries, metrics). This is the same idea as the GoF Decorator, scaled to a whole pod.


3.5 Ambassador

An out-of-process proxy dedicated to one client (typically a legacy or specialised app) that handles protocol translation, retries, sharding, etc. Same pattern as Proxy / Adapter, but at process boundary.


3.6 Sharding

Split one logical dataset across N physical nodes. The two classic strategies are hash-based (uniform distribution) and range-based (range scans friendly).

javascript
const shard = (key, n) =>
  key.split("").reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0) % n;
shard("user_42", 4); // → 2

The hard parts: re-sharding (consistent hashing helps), cross-shard joins (denormalise or do them in the app), hot keys (split by range, add a cache).


3.7 Leader Election

Pick exactly one coordinator among peers. The elected leader holds a lease (often via a lock in ZooKeeper / etcd / Redis + Redlock) and the others stand by.

javascript
// Pseudo-code with a TTL-based lock
async function tryLead(id) {
  const ok = await lock.acquire("scheduler-leader", { ttl: 10_000 });
  if (ok) setInterval(() => lock.renew("scheduler-leader"), 5_000);
}

4. Pattern → Distributed Counterpart Cheat-Sheet

Use this as a flash-card set before an interview.

GoF patternDistributed counterpartWhy the similarity holds
SingletonConnection pool, Service registry entry"One shared thing everyone else reaches for"
FactoryStrategy + config-driven routingHide the choice of backend
BuilderPipeline / chain of transformersStep-by-step construction
PrototypeRead-through cache, snapshot"Copy an already-built thing"
AdapterAnti-corruption layer (DDD)Translate between incompatible systems
DecoratorService mesh sidecar, middleware chainWrap behaviour at the edge
FacadeAPI Gateway / BFFOne simple entry, many subsystems
ProxyCDN, API Gateway, Circuit Breaker wrapperStand-in that controls access
CompositeTree-sharded data, recursive crawlersSame operation over a tree
ObserverPub/Sub over a message brokerFan-out notifications
StrategyPluggable routing/replication policyPick behaviour at runtime
StateCircuit Breaker state machineBehaviour depends on internal state
Chain of ResponsibilityAPI Gateway filters, Saga stepsPass-along pipeline
Template MethodWorkflow engine (Airflow / Temporal)Fix the skeleton, vary the steps
CommandCommand queue, CQRS command sideRequest as a first-class object
MediatorSaga Orchestrator, Service RegistryCentral coordinator
IteratorCursor pagination, Kafka consumerStream items without exposing internals
MementoDB snapshots, Redis RDBSave / restore snapshots

5. How to Choose a Pattern (Decision Flow)


6. Cross-References in This Module

For the foundational GoF-in-Node.js examples, see also Foundations → Design Patterns.

Released under the ISC License.