Design Patterns — Complete Reference

Indrajith's — Reference Documents — April 2026

GoF Creational, Structural and Behavioral patterns, Architectural patterns, and Enterprise integration patterns. For each pattern: intent, diagram, code where useful, when to use, when to avoid, and real-world examples.

Note: A pattern is a solution to a recurring problem in a context. The context is as important as the solution. Every pattern is wrong in the wrong context. Naming a pattern without articulating why this problem calls for it, and what you are giving up, is worse than not naming it at all.
LayerScopeDecision Level
CreationalHow objects are createdClass / method design
StructuralHow objects are composedClass / module design
BehavioralHow objects communicateClass / interaction design
ArchitecturalHow system components are structuredSystem design
EnterpriseHow complex business systems are builtDomain / integration design

Creational Patterns

Creational patterns abstract the instantiation process — hiding how objects are created, composed, and represented.

Singleton GoF — Also known as: Single Instance

Intent: Ensure a class has only one instance, and provide a global point of access to it.

classDiagram class DatabaseConnection { -instance: DatabaseConnection -DatabaseConnection() +getInstance()$ DatabaseConnection +query(sql) Promise } DatabaseConnection --> DatabaseConnection : returns single instance
class DatabaseConnection {
  private static instance: DatabaseConnection | null = null;

  private constructor() {
    // Private — prevents direct instantiation
    this.pool = createPool({ max: 10 });
  }

  public static getInstance(): DatabaseConnection {
    if (!DatabaseConnection.instance) {
      DatabaseConnection.instance = new DatabaseConnection();
    }
    return DatabaseConnection.instance;
  }
}

// Always returns the same instance
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();
console.log(db1 === db2); // true

Use when: You need exactly one shared instance — database connection pools, configuration managers, logging services.

Avoid when: You need to test code in isolation. Singletons carry state between tests, making unit testing painful. Prefer dependency injection of a shared instance in modern applications.

Real-world examples: Node.js module system — every require() returns the same cached module. React Context API. AWS SDK clients — typically initialised once per Lambda invocation.

Factory Method GoF — Also known as: Virtual Constructor

Intent: Define an interface for creating an object, but let subclasses decide which class to instantiate.

classDiagram class NotificationService { +createNotification()* Notification +notify(recipient, message) void } class EmailService { +createNotification() EmailNotification } class SlackService { +createNotification() SlackNotification } class Notification { <<interface>> +send() Promise } NotificationService <|-- EmailService NotificationService <|-- SlackService Notification <|.. EmailNotification Notification <|.. SlackNotification NotificationService ..> Notification
interface Notification {
  send(recipient: string, message: string): Promise<void>;
}

abstract class NotificationService {
  // Factory Method — subclasses implement this
  abstract createNotification(): Notification;

  async notify(recipient: string, message: string) {
    const notification = this.createNotification();
    await notification.send(recipient, message);
  }
}

class EmailService extends NotificationService {
  createNotification() { return new EmailNotification(); }
}

class SlackService extends NotificationService {
  createNotification() { return new SlackNotification(); }
}

Use when: The exact type of object to create is determined at runtime. You want to isolate client code from concrete classes.

Real-world examples: React's createElement. Spring's BeanFactory.getBean(). LoggerFactory.getLogger() in logging frameworks.

Abstract Factory GoF — Also known as: Kit

Intent: Provide an interface for creating families of related or dependent objects without specifying their concrete classes.

Where Factory Method creates one product, Abstract Factory creates a family of related products. The key guarantee: products from the same factory are compatible with each other.

flowchart LR APP["Application\nuses CloudFactory"] --> CF["<<interface>>\nCloudFactory\n+createStorage()\n+createQueue()"] CF --> AF["AWSFactory\n→ S3Storage\n→ SQSQueue"] CF --> AZF["AzureFactory\n→ BlobStorage\n→ ServiceBusQueue"]
interface CloudFactory {
  createStorage(): StorageService;
  createQueue(): QueueService;
}

class AWSFactory implements CloudFactory {
  createStorage() { return new S3Storage(); }
  createQueue()   { return new SQSQueue(); }
}

class AzureFactory implements CloudFactory {
  createStorage() { return new BlobStorage(); }
  createQueue()   { return new ServiceBusQueue(); }
}

// Application code — works with any cloud, swappable at config time
class DocumentProcessor {
  constructor(private cloud: CloudFactory) {}

  async process(doc: Document) {
    const storage = this.cloud.createStorage();
    const queue   = this.cloud.createQueue();
    const url = await storage.upload(doc.id, doc.content);
    await queue.publish(`Processed: ${url}`);
  }
}

Factory Method vs Abstract Factory: Factory Method creates one product type and uses inheritance. Abstract Factory creates multiple related product types and uses composition.

Real-world examples: Java JDBC — each driver creates Connection, Statement, and ResultSet objects guaranteed to be compatible. Terraform providers. UI theme kits.

Builder GoF

Intent: Separate the construction of a complex object from its representation so that the same construction process can create different representations.

Builder replaces telescoping constructors (constructors with many parameters) with a fluent, readable API. The result is typically immutable.

class HttpRequestBuilder {
  private method  = 'GET';
  private headers: Record<string, string> = {};
  private body?: string;
  private timeout = 5000;
  private retries = 0;

  constructor(private readonly url: string) {}

  withMethod(m: string)              { this.method = m; return this; }
  withHeader(k: string, v: string)   { this.headers[k] = v; return this; }
  withBody(b: string)                { this.body = b; return this; }
  withTimeout(ms: number)            { this.timeout = ms; return this; }
  withRetries(n: number)             { this.retries = n; return this; }
  build()                            { return new HttpRequest(this); }
}

const request = new HttpRequestBuilder('https://api.example.com/orders')
  .withMethod('POST')
  .withHeader('Authorization', `Bearer ${token}`)
  .withBody(JSON.stringify(orderData))
  .withTimeout(10000)
  .withRetries(3)
  .build();

Real-world examples: ORM query builders — query.where().orderBy().limit().build(). AWS CDK constructs. Lombok's @Builder annotation in Java.

Prototype GoF

Intent: Specify the kinds of objects to create using a prototypical instance, and create new objects by copying (cloning) this prototype.

Useful when object creation is expensive and you need many similar objects. Clone the expensive one, then modify the clone. The critical design decision is shallow vs deep clone: shallow copies share nested object references; deep copies are fully independent.

Real-world examples: JavaScript spread operator ({ ...obj }) and Object.assign() for shallow cloning. Game engines — clone a base enemy prototype, then modify stats for each variant. Spring prototype-scoped beans.

Object Pool Extended GoF — Also known as: Resource Pool

Intent: Maintain a pool of reusable objects that are expensive to create, lending them out on demand and returning them to the pool after use rather than destroying them.

sequenceDiagram participant C as Client participant P as Pool participant O as Pooled Object C->>P: acquire() alt Object available P->>C: return existing object else Pool empty P->>O: new Object() O-->>P: created P->>C: return new object end C->>C: use object C->>P: release(object) P->>P: validate and return to pool

Key design concerns: Pool size (min/max bounds), validation before lending (is the connection still alive?), timeout behaviour when all objects are in use, and eviction of stale idle objects.

Real-world examples: Database connection pools — HikariCP (Java), pg-pool (Node.js). Thread pools — Java's ExecutorService. HTTP keep-alive — browsers pool TCP connections to the same host.


Structural Patterns

Structural patterns deal with object composition — how classes and objects are assembled to form larger, more flexible structures.

Adapter GoF — Also known as: Wrapper

Intent: Convert the interface of a class into another interface that clients expect. Adapter lets classes work together that could not otherwise because of incompatible interfaces.

flowchart LR CLIENT["Client Code\nexpects PaymentProcessor"] --> ADAPTER["LegacyGatewayAdapter\nimplements PaymentProcessor\nwraps LegacyGateway"] ADAPTER --> LEGACY["LegacyPaymentGateway\nprocessPayment(params)"]
// The interface your application expects
interface PaymentProcessor {
  charge(amount: number, currency: string, token: string): Promise<string>;
}

// The legacy gateway you cannot change
class LegacyPaymentGateway {
  processPayment(params: { amt: number; curr: string; cardToken: string }) {
    return { transId: 'TXN-001', status: 'OK' };
  }
}

// The Adapter bridges the incompatible interfaces
class LegacyGatewayAdapter implements PaymentProcessor {
  constructor(private legacy: LegacyPaymentGateway) {}

  async charge(amount: number, currency: string, token: string): Promise<string> {
    const result = this.legacy.processPayment(
      { amt: amount, curr: currency, cardToken: token }
    );
    if (result.status !== 'OK') throw new Error('Payment failed');
    return result.transId;
  }
}

Real-world examples: Hexagonal Architecture — every port adapter is this pattern. ORMs — Sequelize and TypeORM adapt domain objects to SQL. React's useRef — adapts the DOM's imperative API to React's declarative model.

Bridge GoF — Also known as: Handle / Body

Intent: Decouple an abstraction from its implementation so that the two can vary independently.

Bridge solves the Cartesian product explosion: N abstractions × M implementations = N×M subclasses without Bridge, N+M with Bridge.

flowchart LR subgraph "Without Bridge — N×M classes" CV["CircleVector"] & CR["CircleRaster"] & SV["SquareVector"] & SR["SquareRaster"] end subgraph "With Bridge — N+M classes" C["Circle"] & S["Square"] VR["VectorRenderer"] & RR["RasterRenderer"] C & S --> VR & RR end

Adapter vs Bridge: Adapter makes incompatible interfaces work together — applied after the fact to fix a mismatch. Bridge is designed upfront to let abstractions and implementations vary independently.

Real-world examples: JDBC — java.sql.Connection is the abstraction; each driver is the implementation. React Native — JavaScript components bridged to native iOS/Android implementations.

Composite GoF

Intent: Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.

flowchart TD ROOT["/root (Composite)\ngetSize() = sum of children"] ROOT --> D1["/docs (Composite)\ngetSize() = sum of children"] ROOT --> F1["readme.txt (Leaf)\ngetSize() = 4 KB"] D1 --> F2["report.pdf (Leaf)\ngetSize() = 2 MB"] D1 --> F3["notes.txt (Leaf)\ngetSize() = 12 KB"]

A file and a folder respond to the same getSize() call. The folder sums its children recursively; the file returns its own size. Client code treats both the same way.

Real-world examples: File systems. React component tree — a simple button and a complex layout both render the same way. HTML DOM — every node implements the Node interface.

Decorator GoF — Also known as: Wrapper

Intent: Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

flowchart LR REQ["Request"] --> RT["RetryClient\n3 attempts"] --> AU["AuthClient\nadds Bearer token"] --> LG["LoggingClient\nlogs request"] --> FC["FetchClient\nbase HTTP"] --> API
interface HttpClient {
  get(url: string): Promise<Response>;
}

// Decorator 1: adds logging
class LoggingClient implements HttpClient {
  constructor(private inner: HttpClient) {}
  async get(url: string) {
    console.log(`GET ${url}`);
    const res = await this.inner.get(url);
    console.log(`${res.status} ${url}`);
    return res;
  }
}

// Decorator 2: adds auth
class AuthClient implements HttpClient {
  constructor(private inner: HttpClient, private token: string) {}
  get(url: string) {
    // In practice: attach header to outgoing request
    return this.inner.get(url);
  }
}

// Stack decorators — order matters
const client: HttpClient =
  new AuthClient(
    new LoggingClient(
      new FetchClient()
    ), token
  );

Real-world examples: Express.js / Koa middleware — each app.use() adds a decorator. Python decorators (@functools.wraps). Java I/O streams — new BufferedReader(new InputStreamReader(new FileInputStream(...))).

Facade GoF

Intent: Provide a simplified interface to a complex subsystem. A Facade does not hide the subsystem — it provides a convenient layer for the most common use cases.

flowchart LR CLIENT["Client\norderFacade.placeOrder(order)"] --> FACADE["OrderFacade\none method, coordinates all"] FACADE --> INV["InventoryService"] FACADE --> PAY["PaymentService"] FACADE --> SHIP["ShippingService"] FACADE --> NOTIF["NotificationService"]

Real-world examples: AWS SDK, Stripe SDK, Twilio SDK. jQuery — a facade over the inconsistent browser DOM API. Service layer in layered architecture — a facade over the domain model and repositories.

Flyweight GoF

Intent: Use sharing to support a large number of fine-grained objects efficiently by externalising the parts of state that are common across many instances.

Flyweight separates state into intrinsic (shared, immutable — stored in the flyweight) and extrinsic (unique per use — passed in by the caller). Reduces memory from O(n×m) to O(n+m).

Real-world examples: Java and Python string interning. Java Integer cache for values -128 to 127. Game engines — particles share a single mesh/texture (intrinsic), each with unique position/velocity (extrinsic).

Proxy GoF — Also known as: Surrogate

Intent: Provide a surrogate or placeholder for another object to control access to it.

TypeWhat It Does
Virtual ProxyDelays expensive object creation until first use (lazy loading)
Protection ProxyControls access based on permissions before delegating
Remote ProxyRepresents an object in a different process or machine; handles serialisation

Proxy vs Decorator vs Adapter: Proxy controls access to the same interface — proxy and subject are interchangeable. Decorator adds new behaviour to the same interface. Adapter changes the interface. All three wrap an object; the difference is intent.

Real-world examples: JavaScript new Proxy(target, handler) — used by Vue.js for reactivity. ORM lazy-loading — SQL deferred until the relationship is accessed. Service meshes (Envoy/Istio) — the sidecar proxy acts as a Remote + Protection Proxy.


Behavioral Patterns

Behavioral patterns are concerned with algorithms and the assignment of responsibilities between objects.

Chain of Responsibility GoF

Intent: Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along until one handles it.

flowchart LR REQ["Incoming Request"] --> AUTH["AuthHandler\nCheck token"] AUTH -- "Valid" --> RATE["RateLimitHandler\nCheck IP"] AUTH -- "Invalid" --> E1["401 Unauthorized"] RATE -- "OK" --> BL["BusinessHandler\nProcess request"] RATE -- "Exceeded" --> E2["429 Too Many Requests"] BL --> RESP["200 OK"]
abstract class RequestHandler {
  private next?: RequestHandler;

  setNext(handler: RequestHandler): RequestHandler {
    this.next = handler;
    return handler; // enables chaining: a.setNext(b).setNext(c)
  }

  async handle(req: Request): Promise<Response> {
    if (this.next) return this.next.handle(req);
    throw new Error('No handler for request');
  }
}

class AuthHandler extends RequestHandler {
  async handle(req: Request) {
    if (!req.headers.authorization) throw new Error('401 Unauthorized');
    return super.handle(req); // pass to next handler
  }
}

// Wire the chain
const auth = new AuthHandler();
auth.setNext(new RateLimitHandler()).setNext(new BusinessLogicHandler());
const response = await auth.handle(incomingRequest);

Real-world examples: Express.js / Koa middleware stack. Spring Security filter chain. DOM event bubbling.

Command GoF — Also known as: Action, Transaction

Intent: Encapsulate a request as an object, thereby letting you parameterise clients with different requests, queue or log requests, and support undoable operations.

Command turns a method call into a first-class object. This gives you: deferred execution, undo/redo, macros, comprehensive logging, and transactions.

Real-world examples: Redux actions — every action is a Command object; the reducer is the Receiver. SQL transactions — BEGIN/COMMIT/ROLLBACK. Job queues (Bull, Sidekiq, Celery). Git commits — each commit can be reverted with git revert.

Iterator GoF

Intent: Provide a way to sequentially access elements of a collection without exposing its underlying representation.

The client uses the same next() / hasNext() interface whether iterating an array, a linked list, a database cursor, or an infinite lazy sequence. In modern languages this is the built-in iteration protocol: JavaScript's Symbol.iterator, Python's __iter__, Java's Iterable.

Real-world examples: JavaScript generators (function*) — lazy iterators that produce values on demand. Database cursors — stream result sets without loading all rows into memory. API pagination cursors.

Mediator GoF

Intent: Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly.

Without Mediator, a system of N components has O(N²) relationships. With Mediator, each component communicates only with the Mediator: O(N) relationships.

Real-world examples: Message brokers (Kafka, RabbitMQ) — services publish to the broker and subscribe from it; they never communicate directly. Air traffic control. Redux store.

Memento GoF

Intent: Without violating encapsulation, capture and externalise an object's internal state so that the object can be restored to that state later.

The originator creates a memento snapshot; a caretaker stores it; the originator can restore itself from any stored memento. This enables undo/redo without exposing the internals of the object being saved.

Real-world examples: Text editor undo — every keystroke creates a Memento; Ctrl+Z restores the previous one. Git commits — each commit is a Memento; git checkout <hash> restores it. Database savepoints.

Observer GoF — Also known as: Publish-Subscribe, Event Listener

Intent: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

flowchart TD SUBJ["Subject (EventBus)\nemit('order.placed', data)"] --> OB1["Observer 1\nEmailService"] SUBJ --> OB2["Observer 2\nInventoryService"] SUBJ --> OB3["Observer 3\nAnalyticsService"]
type EventMap = {
  'order.placed': { orderId: string; amount: number };
  'payment.failed': { orderId: string; reason: string };
};

class TypedEventBus<T extends Record<string, any>> {
  private listeners = new Map<string, Set<Function>>();

  on<K extends keyof T>(event: K, listener: (data: T[K]) => void) {
    if (!this.listeners.has(event as string))
      this.listeners.set(event as string, new Set());
    this.listeners.get(event as string)!.add(listener);
  }

  emit<K extends keyof T>(event: K, data: T[K]) {
    this.listeners.get(event as string)?.forEach(l => l(data));
  }
}

const bus = new TypedEventBus<EventMap>();
bus.on('order.placed', ({ orderId }) => emailService.sendConfirmation(orderId));
bus.on('order.placed', ({ orderId }) => inventoryService.reserveItems(orderId));
bus.emit('order.placed', { orderId: 'ord_123', amount: 99.99 });

Real-world examples: React's useState/useEffect. RxJS — reactive programming formalised. DOM addEventListener. Kafka / RabbitMQ — distributed Observer across services.

State GoF

Intent: Allow an object to alter its behaviour when its internal state changes. The object will appear to change its class.

stateDiagram-v2 [*] --> Pending : Order created Pending --> Confirmed : Payment authorised Confirmed --> Shipped : Items dispatched Shipped --> Delivered : Delivery confirmed Delivered --> Returned : Return initiated Pending --> Cancelled : Payment failed Confirmed --> Cancelled : Manually cancelled Returned --> [*] Delivered --> [*] Cancelled --> [*]

State vs Strategy: Strategy — the algorithm is chosen externally and typically does not change during the object's life. State — states know about each other and trigger transitions in response to events.

Real-world examples: XState library — formal finite state machines for JavaScript. TCP connection — LISTEN, SYN_SENT, ESTABLISHED, CLOSE_WAIT. AWS Step Functions / Temporal.

Strategy GoF — Also known as: Policy

Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

classDiagram class PaymentContext { -strategy: PaymentStrategy +setStrategy(s) void +pay(amount) void } class PaymentStrategy { <<interface>> +pay(amount) Promise } class CreditCardStrategy { +pay(amount) Promise } class PayPalStrategy { +pay(amount) Promise } class CryptoStrategy { +pay(amount) Promise } PaymentContext --> PaymentStrategy PaymentStrategy <|.. CreditCardStrategy PaymentStrategy <|.. PayPalStrategy PaymentStrategy <|.. CryptoStrategy

Strategy replaces large if/switch blocks with polymorphism. Instead of if (type === 'credit') { ... } else if (type === 'paypal') { ... }, each payment method is a separate strategy object.

Real-world examples: Passport.js authentication — JWTStrategy, OAuth2Strategy, LocalStrategy. Compression — GzipStrategy, ZstdStrategy, BrotliStrategy selected based on content type.

Template Method GoF

Intent: Define the skeleton of an algorithm in a base class, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps without changing the algorithm's structure.

abstract class ReportGenerator {
  // Template Method — algorithm skeleton, fixed sequence
  async generate(query: ReportQuery): Promise<string> {
    const raw    = await this.fetchData(query);  // step 1: fixed
    const clean  = this.transform(raw);          // step 2: fixed
    const output = this.format(clean);           // step 3: varies by subclass
    this.onComplete(query);                      // hook: optional override
    return output;
  }

  private async fetchData(q: ReportQuery) { /* DB query */ return []; }
  private transform(data: any[]) { return data.filter(Boolean); }

  protected abstract format(data: any[]): string;
  protected onComplete(q: ReportQuery): void {}  // default no-op
}

class CsvReportGenerator extends ReportGenerator {
  protected format(data: any[]) {
    return data.map(row => Object.values(row).join(',')).join('\n');
  }
}

class JsonReportGenerator extends ReportGenerator {
  protected format(data: any[]) { return JSON.stringify(data, null, 2); }
}

Real-world examples: React component lifecycle — componentDidMount, componentDidUpdate are Template Method hooks. Jest — beforeAll, beforeEach, afterEach. Spring's JdbcTemplate.

Visitor GoF

Intent: Represent an operation to be performed on elements of an object structure. Visitor lets you define new operations without changing the classes of the elements on which it operates.

Visitor solves the double-dispatch problem. Add a new operation (export, validate, render) without modifying the element classes. Trade-off: adding a new element type requires updating every visitor.

Real-world examples: Abstract Syntax Trees — Babel and ESLint traverse an AST; visitors perform transformation or linting without modifying node classes. Compiler optimisation passes.

Interpreter GoF

Intent: Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.

Specialised — use it when you need to evaluate expressions or queries defined by a simple grammar. Each grammar rule becomes a class with an interpret(context) method. Not suitable for complex grammars.

Real-world examples: SQL parsers — each SQL clause is an Interpreter class. Regular expression engines. Spring Expression Language (SpEL) — #{user.age > 18}.


Architectural Patterns

Architectural patterns operate at the system level — they describe how to organise entire applications and services.

MVC / MVP / MVVM Architectural

Intent: Separate the user interface (View) from the application logic (Controller/Presenter/ViewModel) and the data (Model) to allow independent development, testing, and evolution of each concern.

VariantWho Controls the ViewData FlowUsed In
MVCController handles input; View observes Model directlyModel → View (direct)Rails, Django, Spring MVC, Angular
MVPPresenter fully controls View through an interfacePresenter → View (via interface)Android (legacy), WinForms
MVVMView binds to ViewModel's observable stateViewModel ↔ View (data binding)React+Redux, Vue, WPF, SwiftUI

CQRS Command Query Responsibility Segregation

Intent: Separate the read model (queries) from the write model (commands). Commands change state and return nothing; queries return state and change nothing.

flowchart LR subgraph "WRITE SIDE" CMD["Command\nPlaceOrder"] --> CH["Command Handler"] CH --> WDB[("Write DB\nNormalised, ACID")] CH --> EVT["Domain Events"] end subgraph "READ SIDE" EVT -->|"project events"| RDB[("Read Model\nDenormalised")] QRY["Query\nGetOrderSummary"] --> RDB end
When It Pays OffWhen It Costs Too Much
Read/write ratio is highly asymmetricSimple CRUD applications
Read models need very different data shapesSmall teams without operational maturity
Need to scale reads independently from writesWhen eventual consistency is unacceptable

Event Sourcing Architectural

Intent: Store the state of an application as a sequence of immutable events rather than the current state. Current state is always derived by replaying the event log.

flowchart LR subgraph "Immutable Event Log" E1["OrderPlaced\nt=1"] --> E2["PaymentReceived\nt=2"] --> E3["ItemShipped\nt=3"] --> E4["ItemDelivered\nt=4"] end E4 --> PROJ["Replay events\n→ current state"] PROJ --> STATE["Order { status: Delivered }"]
What You GainWhat It Costs
Full audit trail — every state change recordedSchema evolution is hard; events must be versioned
Time travel — reconstruct state at any point in historyLong event streams slow replay; mitigated by snapshots
Event replay — build new read models from historyCannot query event store directly; need projections

Real-world examples: Git — each commit is an immutable event; the codebase state is derived by replaying commits. Double-entry bookkeeping — balance derived by summing immutable ledger entries. Kafka as an event store.

Saga Architectural

Intent: Manage distributed transactions across multiple services without two-phase commit, coordinating through local transactions with compensating transactions for failure recovery.

sequenceDiagram participant O as OrderService participant P as PaymentService participant I as InventoryService O->>O: Create order (local txn) O->>P: OrderPlaced event P->>P: Charge card (local txn) P->>I: PaymentSucceeded event I->>I: Reserve stock (local txn) Note over I: If stock unavailable: I->>P: StockFailed event P->>P: Refund card (compensating txn) P->>O: PaymentRefunded event O->>O: Cancel order (compensating txn)
Choreography-basedOrchestration-based
No central coordinator; services react to eventsCentral orchestrator directs each step
Simpler to implement; harder to debugExplicit control flow; single point of failure risk
Risk of cyclic event dependenciesUsed by Temporal, AWS Step Functions

Strangler Fig Architectural — Also known as: Strangler Application

Intent: Incrementally migrate a legacy system by gradually replacing specific pieces with new services while routing traffic between old and new, until the legacy system can be decommissioned.

flowchart LR subgraph "Phase 1" C1["Client"] --> PX1["Proxy"] --> LEG1["Legacy (all traffic)"] end subgraph "Phase 2" C2["Client"] --> PX2["Proxy"] PX2 --> NEW["New Service (some traffic)"] PX2 --> LEG2["Legacy (rest)"] end subgraph "Phase 3" C3["Client"] --> NEW2["New System (all traffic)"] end

Real-world examples: Uber's migration — Strangler Fig applied to a Python monolith over multiple years. Booking.com — migrated from a Perl monolith over a decade. Every successful microservices migration uses this pattern; the failed ones attempted big-bang rewrites.

Backend for Frontend (BFF) Architectural — Also known as: API Gateway per Client Type

Intent: Create a separate backend service for each type of frontend client, tailored to that client's specific data and interaction needs.

flowchart LR MOB["Mobile App"] --> MBFF["Mobile BFF\nLight payloads"] WEB["Web App"] --> WBFF["Web BFF\nRich data"] API["Partner API"] --> ABFF["API BFF\nVersioned"] MBFF & WBFF & ABFF --> MS["Internal Microservices\nOrder, User, Product"]

Real-world examples: Netflix — different device types each have separate BFFs. SoundCloud — coined and popularised BFF. GraphQL as a BFF — clients query exactly the fields they need.

Sidecar Architectural

Intent: Deploy auxiliary components (logging, monitoring, proxying, security) alongside application services as a separate process in the same execution environment, sharing the same lifecycle.

flowchart LR subgraph "Kubernetes Pod" APP["Application Container\nbusiness logic"] <--> SC["Sidecar Container\nEnvoy proxy"] end EXT["External Traffic"] --> SC SC -- "mTLS, tracing, retries" --> OTHER["Other Services"]

Real-world examples: Envoy proxy (Istio service mesh) — each pod gets a sidecar handling mTLS, tracing, retries, and circuit breaking with no application code changes. Datadog agent as a sidecar.

Anti-Corruption Layer DDD — Also known as: ACL, Translation Layer

Intent: Create an isolation layer between your domain model and an external system to prevent foreign concepts, terminology, and structures from leaking into your domain and corrupting it.

flowchart LR subgraph "Your Clean Domain" C["Customer"] & O["Order"] & P["Product"] end subgraph "Anti-Corruption Layer" CT["CustomerTranslator"] & OT["OrderTranslator"] & PT["ProductTranslator"] end subgraph "Legacy ERP" CR["CLIENT_RECORD"] & OH["ORD_HDR_TBL"] & IM["ITEM_MASTER_V2"] end C <--> CT <--> CR O <--> OT <--> OH P <--> PT <--> IM

Real-world examples: ERP migration — the ERP uses CUST_ACCT and ORD_HDR; your domain uses Customer and Order; the ACL translates. Third-party payment providers — Stripe uses PaymentIntent; your domain uses Transaction.


Enterprise Patterns

Enterprise patterns address recurring challenges in large-scale business software: managing complex domains, ensuring data consistency, handling failures gracefully, and scaling under load.

Repository DDD — Fowler

Intent: Mediate between the domain and data mapping layers using a collection-like interface for accessing domain objects, abstracting the underlying data store completely from the domain model.

// Domain-facing interface — no SQL, no ORM, no database concepts
interface OrderRepository {
  findById(id: string): Promise<Order | null>;
  findByCustomer(customerId: string): Promise<Order[]>;
  save(order: Order): Promise<void>;
}

// Production implementation — PostgreSQL
class PostgresOrderRepository implements OrderRepository {
  async findById(id: string) {
    const row = await this.db.query('SELECT * FROM orders WHERE id=$1', [id]);
    return row ? this.toDomain(row) : null;
  }
  async save(order: Order) {
    await this.db.query(
      `INSERT INTO orders (id, customer_id, total, status) VALUES ($1,$2,$3,$4)
       ON CONFLICT (id) DO UPDATE SET total=$3, status=$4`,
      [order.id, order.customerId, order.total, order.status]
    );
  }
  // findByCustomer omitted for brevity
}

// Test implementation — zero infrastructure needed
class InMemoryOrderRepository implements OrderRepository {
  private store = new Map<string, Order>();
  async findById(id: string) { return this.store.get(id) ?? null; }
  async save(order: Order)   { this.store.set(order.id, order); }
  async findByCustomer(cid: string) {
    return [...this.store.values()].filter(o => o.customerId === cid);
  }
}

Unit of Work Fowler

Intent: Maintain a list of objects affected by a business transaction and coordinate the writing out of changes as a single atomic operation.

Unit of Work tracks all objects added, modified, or deleted during a business operation, then flushes all changes to the database in a single transaction. Without it, each repository manages its own transaction, and complex operations can leave the database in inconsistent states.

Real-world examples: Entity Framework's DbContextSaveChanges() flushes all tracked changes atomically. Hibernate's Session. SQLAlchemy's session object.

Aggregate DDD — Also known as: Aggregate Root

Intent: Cluster a group of related domain objects into a consistency boundary. The Aggregate Root is the only entry point through which the cluster can be modified, ensuring all business invariants are enforced.

flowchart TB subgraph "Order Aggregate" OR["Order (Root)\nid, customerId, status, total"] OI1["OrderItem\nproductId, qty, price"] OI2["OrderItem\nproductId, qty, price"] SA["ShippingAddress"] OR --> OI1 & OI2 & SA end OR -. "Reference by ID only" .-> CUST["Customer Aggregate"]

Rules: Reference other aggregates by ID only — never by object reference. Transactions must not span aggregate boundaries. Keep aggregates small — large aggregates create lock contention. Ask: "What must be consistent at the same time?"

Domain Events DDD

Intent: Capture significant occurrences within the domain as first-class objects, allowing other parts of the system to react to them without the originating object knowing about the side effects.

Events are named in the past tense (they have already happened; they are immutable facts): OrderPlaced, PaymentFailed, CustomerUpgraded.

Domain Events vs Integration Events: Domain Events are raised and handled within the same bounded context, often synchronously in the same transaction. Integration Events cross bounded context boundaries and are published to a message broker for other services to consume asynchronously. Domain Events often become Integration Events.

Specification DDD

Intent: Encapsulate a business rule as a reusable, combinable object that can test whether a candidate object satisfies criteria — and be used for both in-memory validation and database querying.

Business rules such as "an order is eligible for free shipping if its total exceeds £50, the customer is a premium member, and the destination is within the UK" are encapsulated as Specification objects and combined with AND, OR, and NOT operations.

Real-world examples: Ardalis.Specification (C#) — popular open-source implementation for Entity Framework. LINQ expressions that translate to SQL WHERE clauses. Feature flags — a feature flag is a Specification evaluating whether a user satisfies criteria for enabling a feature.

Transactional Outbox Distributed Systems — Also known as: Outbox Pattern

Intent: Reliably publish domain events to a message broker by first writing them to an outbox table in the same database transaction as the business operation, then publishing them separately.

flowchart TD subgraph "Single Atomic DB Transaction" ORD["INSERT into orders"] & OUT["INSERT into outbox_events"] end ORD & OUT --> COMMIT["COMMIT"] COMMIT --> RELAY["Outbox Relay Process\nPolls outbox, publishes to broker, marks published"] RELAY --> BROKER["Message Broker"] BROKER --> SUBS["Downstream Services"]

Problem it solves: Without the Outbox, saving an order and publishing a Kafka event are two separate operations. If the DB write succeeds but Kafka publish fails, you have an order with no event. The Outbox makes both writes atomic, then publishes reliably after commit.

Real-world examples: Debezium — implements Outbox via Change Data Capture; reads the database transaction log and publishes to Kafka. MassTransit Outbox (.NET). Any system claiming "at-least-once delivery" with database-backed state is using this pattern or an equivalent.

Circuit Breaker Resilience

Intent: Prevent an application from repeatedly trying to execute an operation that is likely to fail, allowing it to continue without waiting for the fault to be corrected while the downstream service recovers.

stateDiagram-v2 [*] --> Closed : Initial state Closed --> Open : Failure count exceeds threshold Open --> HalfOpen : Timeout period elapsed HalfOpen --> Closed : Test request succeeds HalfOpen --> Open : Test request fails Closed : CLOSED — requests pass through; failures tracked Open : OPEN — requests fail immediately; fallback returned; no network calls HalfOpen : HALF-OPEN — one test request sent

Real-world examples: Resilience4j (Java) — modern successor to Netflix Hystrix. Polly (.NET) — Circuit Breaker, Retry, and Bulkhead policies. Envoy proxy — circuit breaking at the network layer with no application code changes.

Bulkhead Resilience

Intent: Isolate elements of an application into pools so that if one fails, the others continue to function — preventing a failure from cascading to consume all available shared resources.

Named after watertight compartments in a ship's hull. If your user-facing API and your internal reporting system share a single thread pool, a spike in report generation starves user-facing request threads. Bulkheads give each concern its own isolated pool.

Real-world examples: Separate thread pools per downstream dependency. Kubernetes resource requests/limits — pods are bulkheads; limits prevent one pod from starving another. Microservices themselves — at the architecture level, they are bulkheads.

Retry with Exponential Backoff Resilience

Intent: Handle transient failures by automatically retrying a failed operation after a delay, with exponentially increasing delays and random jitter to prevent the thundering herd problem.

async function withRetry<T>(
  operation: () => Promise<T>,
  maxAttempts = 3,
  baseDelayMs = 100
): Promise<T> {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await operation();
    } catch (error) {
      if (attempt === maxAttempts) throw error;
      if (!isTransient(error)) throw error; // don't retry 4xx

      // Exponential backoff: 100ms, 200ms, 400ms...
      const delay = baseDelayMs * Math.pow(2, attempt - 1);
      // Jitter: randomise ±25% to spread retries across clients
      const jitter = delay * (0.75 + Math.random() * 0.5);
      await new Promise(res => setTimeout(res, jitter));
    }
  }
  throw new Error('Unreachable');
}

function isTransient(error: any): boolean {
  // Only retry network errors and 5xx, never 4xx
  return error.code === 'ECONNRESET' || (error.status >= 500 && error.status < 600);
}
Warning — Idempotency Requirement: Only retry idempotent operations. An idempotent operation produces the same result no matter how many times it is called. If you retry a non-idempotent operation such as chargeCard, you risk charging the customer multiple times. Ensure operations use idempotency keys, or only retry operations that are inherently idempotent.

Cache-Aside Performance — Also known as: Lazy Loading

Intent: Load data into the cache on demand. The application checks the cache first; on a miss, it loads from the data store, populates the cache, and returns the data for subsequent requests.

flowchart TD REQ["Request"] --> CC{"Cache hit?"} CC -- "Hit" --> RET["Return cached data"] CC -- "Miss" --> DB["Query database"] DB --> POP["Populate cache with TTL"] POP --> RET2["Return data"]
StrategyDescriptionTrade-off
Cache-Aside (lazy)Cache populated on first readRisk of stampede on cold start or mass expiry
Write-ThroughCache populated on every writeAlways warm; writes require two operations
Write-BehindWrite to cache immediately, persist asyncFast writes; risk of data loss if cache fails before write completes

Real-world examples: Redis + PostgreSQL — check Redis; on miss query PostgreSQL; write back with TTL. CDN caching — CloudFront serves cached responses; on miss, fetches from origin. Browser HTTP caching per Cache-Control headers.


Patterns are vocabulary, not solutions. The architect's job is knowing when the benefit of a pattern is worth its cost. — Indrajith's — Reference Documents — April 2026