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.
| Layer | What you manipulate | Why it matters at scale |
|---|---|---|
| 🧱 Code object | Class instances, function calls | A bug here affects one process. |
| 🧩 GoF pattern | Objects in memory (Singleton, Factory, Observer, …) | Reusable solution to local complexity. |
| 🏢 Service | APIs, threads, queues owned by one team | A bug here affects one service / one team. |
| 🌐 Distributed pattern | Services across machines (Circuit Breaker, Saga, CQRS, …) | A bug here can cascade across the company. |
| 🏭 Data center | Regions, replication, sharding | A 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).
// 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.
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.
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).
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.
// 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).
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.
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/elsesoup. - When to use: Cache eviction (LRU / LFU / FIFO), routing policies, pricing rules.
- Distributed counterpart: Pluggable routing/replication policy (consistent hash vs round-robin).
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.
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:
| Pattern | Intent | System-design echo |
|---|---|---|
| Command | Encapsulate a request as an object | Command queue / CQRS command side |
| Mediator | Central hub coordinates peers | Service registry / Orchestrator in Saga |
| Iterator | Stream items without exposing internals | Cursor-based pagination / Kafka consumer iterator |
| Memento | Save/restore snapshots | Snapshotting (DB, Redis RDB) |
| Visitor | Add operations without changing the class | Linting / static analysis passes |
| Interpreter | Grammar for a custom language | Rule 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.
| # | Pattern | One-line summary | Deep dive |
|---|---|---|---|
| 1 | Circuit Breaker | Stop calling a downstream that is failing, recover slowly. | circuit-breakers |
| 2 | Saga | Distributed transaction built from local steps + compensations. | saga-pattern |
| 3 | Event Sourcing | Persist every change as an immutable event; derive state on read. | event-sourcing |
| 4 | CQRS | Separate write and read models for the same domain. | § 3.1 below |
| 5 | Bulkhead | Isolate failures into separate resource pools. | § 3.2 below |
| 6 | Retry + Exponential Backoff | Retry with growing + jittered delay. | exponential-backoff |
| 7 | Idempotency Keys | Deduplicate retries of mutating operations. | idempotency-keys |
| 8 | Outbox | Atomically write a DB change and a message to publish. | § 3.3 below |
| 9 | Sidecar | Run a helper container/process next to the main one. | § 3.4 below |
| 10 | Ambassador | Out-of-process proxy that handles cross-cutting network concerns. | § 3.5 below |
| 11 | Sharding | Split one huge dataset across many smaller ones. | § 3.6 below |
| 12 | Leader Election | Elect 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.
// 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.
// 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.
-- 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).
const shard = (key, n) =>
key.split("").reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0) % n;
shard("user_42", 4); // → 2The 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.
// 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 pattern | Distributed counterpart | Why the similarity holds |
|---|---|---|
| Singleton | Connection pool, Service registry entry | "One shared thing everyone else reaches for" |
| Factory | Strategy + config-driven routing | Hide the choice of backend |
| Builder | Pipeline / chain of transformers | Step-by-step construction |
| Prototype | Read-through cache, snapshot | "Copy an already-built thing" |
| Adapter | Anti-corruption layer (DDD) | Translate between incompatible systems |
| Decorator | Service mesh sidecar, middleware chain | Wrap behaviour at the edge |
| Facade | API Gateway / BFF | One simple entry, many subsystems |
| Proxy | CDN, API Gateway, Circuit Breaker wrapper | Stand-in that controls access |
| Composite | Tree-sharded data, recursive crawlers | Same operation over a tree |
| Observer | Pub/Sub over a message broker | Fan-out notifications |
| Strategy | Pluggable routing/replication policy | Pick behaviour at runtime |
| State | Circuit Breaker state machine | Behaviour depends on internal state |
| Chain of Responsibility | API Gateway filters, Saga steps | Pass-along pipeline |
| Template Method | Workflow engine (Airflow / Temporal) | Fix the skeleton, vary the steps |
| Command | Command queue, CQRS command side | Request as a first-class object |
| Mediator | Saga Orchestrator, Service Registry | Central coordinator |
| Iterator | Cursor pagination, Kafka consumer | Stream items without exposing internals |
| Memento | DB snapshots, Redis RDB | Save / restore snapshots |
5. How to Choose a Pattern (Decision Flow)
6. Cross-References in This Module
- Connecting System Design with Design Patterns — Observer ↔ Pub/Sub evolution
- Circuit Breakers — full state-machine + sequence diagrams
- Saga Pattern — orchestration vs choreography
- Event Sourcing — append-only + read models
- Retries with Exponential Backoff — retry math + jitter
- Idempotency Keys — duplicate-charge prevention
For the foundational GoF-in-Node.js examples, see also Foundations → Design Patterns.
