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.
| Layer | Scope | Decision Level |
|---|---|---|
| Creational | How objects are created | Class / method design |
| Structural | How objects are composed | Class / module design |
| Behavioral | How objects communicate | Class / interaction design |
| Architectural | How system components are structured | System design |
| Enterprise | How complex business systems are built | Domain / 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.
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.
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.
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.
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.
// 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.
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.
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.
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.
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.
| Type | What It Does |
|---|---|
| Virtual Proxy | Delays expensive object creation until first use (lazy loading) |
| Protection Proxy | Controls access based on permissions before delegating |
| Remote Proxy | Represents 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.
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.
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.
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.
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.
| Variant | Who Controls the View | Data Flow | Used In |
|---|---|---|---|
| MVC | Controller handles input; View observes Model directly | Model → View (direct) | Rails, Django, Spring MVC, Angular |
| MVP | Presenter fully controls View through an interface | Presenter → View (via interface) | Android (legacy), WinForms |
| MVVM | View binds to ViewModel's observable state | ViewModel ↔ 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.
| When It Pays Off | When It Costs Too Much |
|---|---|
| Read/write ratio is highly asymmetric | Simple CRUD applications |
| Read models need very different data shapes | Small teams without operational maturity |
| Need to scale reads independently from writes | When 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.
| What You Gain | What It Costs |
|---|---|
| Full audit trail — every state change recorded | Schema evolution is hard; events must be versioned |
| Time travel — reconstruct state at any point in history | Long event streams slow replay; mitigated by snapshots |
| Event replay — build new read models from history | Cannot 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.
| Choreography-based | Orchestration-based |
|---|---|
| No central coordinator; services react to events | Central orchestrator directs each step |
| Simpler to implement; harder to debug | Explicit control flow; single point of failure risk |
| Risk of cyclic event dependencies | Used 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.
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.
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.
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.
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 DbContext — SaveChanges() 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.
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.
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.
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.
| Strategy | Description | Trade-off |
|---|---|---|
| Cache-Aside (lazy) | Cache populated on first read | Risk of stampede on cold start or mass expiry |
| Write-Through | Cache populated on every write | Always warm; writes require two operations |
| Write-Behind | Write to cache immediately, persist async | Fast 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