
Design Patterns
- 102 installs
- 49 repo stars
- Updated February 11, 2026
- ratacat/claude-skills
Helps with design & ui/ux tasks.
About
design-patterns is a Claude Code skill for design & ui/ux. It helps solo builders move faster with AI-assisted coding.
- design-patterns
- Design & UI/UX
- AI-coding skill
Design Patterns by the numbers
- 102 all-time installs (skills.sh)
- Ranked #1,125 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ratacat/claude-skills --skill design-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 102 |
|---|---|
| repo stars | ★ 49 |
| Last updated | February 11, 2026 |
| Repository | ratacat/claude-skills ↗ |
What it does
Helps with design & ui/ux tasks.
Files
Design Patterns
Overview
Design patterns are proven solutions to recurring software design problems. They provide a shared vocabulary for discussing design and capture collective wisdom refined through decades of real-world use.
Core Philosophy: Patterns are templates you adapt to your context, not blueprints to copy. Use the right pattern when it genuinely simplifies your design—not to impress or over-engineer.
Foundational Principles
These principles underpin all good design:
| Principle | Meaning | Violation Symptom |
|---|---|---|
| Encapsulate What Varies | Isolate changing parts from stable parts | Changes ripple through codebase |
| Program to Interfaces | Depend on abstractions, not concretions | Can't swap implementations |
| Composition Over Inheritance | Build behavior by composing objects | Deep rigid class hierarchies |
| Loose Coupling | Minimize interdependency between objects | Can't change one thing without breaking another |
| Open-Closed | Open for extension, closed for modification | Must edit existing code for new features |
| Single Responsibility | One reason to change per class | Classes doing too many things |
| Dependency Inversion | High-level modules don't depend on low-level | Business logic coupled to infrastructure |
Pattern Selection Guide
By Problem Type
CREATING OBJECTS
├── Complex/conditional creation ──────────→ Factory Method
├── Families of related objects ───────────→ Abstract Factory
├── Step-by-step construction ─────────────→ Builder
├── Clone existing objects ────────────────→ Prototype
└── Single instance needed ────────────────→ Singleton (use sparingly!)
STRUCTURING/COMPOSING OBJECTS
├── Incompatible interface ────────────────→ Adapter
├── Simplify complex subsystem ────────────→ Facade
├── Tree/hierarchy structure ──────────────→ Composite
├── Add behavior dynamically ──────────────→ Decorator
└── Control access to object ──────────────→ Proxy
MANAGING COMMUNICATION/BEHAVIOR
├── One-to-many notification ──────────────→ Observer
├── Encapsulate requests as objects ───────→ Command
├── Behavior varies by internal state ─────→ State
├── Swap algorithms at runtime ────────────→ Strategy
├── Algorithm skeleton with hooks ─────────→ Template Method
├── Reduce N-to-N communication ───────────→ Mediator
└── Sequential handlers ───────────────────→ Chain of Responsibility
MANAGING DATA ACCESS
├── Abstract data source ──────────────────→ Repository
├── Track changes for atomic commit ───────→ Unit of Work
├── Ensure object identity ────────────────→ Identity Map
├── Defer expensive loading ───────────────→ Lazy Load
├── Map objects to database ───────────────→ Data Mapper
└── Shape data for transfer ───────────────→ DTOBy Symptom
| Symptom | Consider |
|---|---|
| Giant switch/if-else on type | Strategy, State, or polymorphism |
| Duplicate code across classes | Template Method, Strategy |
| Need to notify many objects of changes | Observer |
| Complex object creation logic | Factory, Builder |
| Adding features bloats class | Decorator |
| Third-party API doesn't fit your code | Adapter |
| Too many dependencies between components | Mediator, Facade |
| Can't test without database/network | Repository, Dependency Injection |
| Need undo/redo | Command |
| Object behavior depends on state | State |
| Request needs processing by multiple handlers | Chain of Responsibility |
Domain Logic: Transaction Script vs Domain Model
| Factor | Transaction Script | Domain Model |
|---|---|---|
| Logic complexity | Simple (< 500 lines) | Complex, many rules |
| Business rules | Few, straightforward | Many, interacting |
| Operations | CRUD-heavy | Rich behavior |
| Team/timeline | Small team, quick delivery | Long-term maintenance |
| Testing | Integration tests | Unit tests on domain |
Rule of thumb: Start with Transaction Script. Refactor to Domain Model when procedural code becomes hard to maintain.
Quick Reference
Tier 1: Essential Patterns (Master First)
| Pattern | One-Line | When to Use | Reference |
|---|---|---|---|
| Strategy | Encapsulate interchangeable algorithms | Multiple ways to do something, swap at runtime | strategy.md |
| Observer | Notify dependents of state changes | Event systems, reactive updates | observer.md |
| Factory | Encapsulate object creation | Complex/conditional instantiation | factory.md |
| Decorator | Add behavior dynamically | Extend without inheritance | decorator.md |
| Command | Encapsulate requests as objects | Undo/redo, queuing, logging | command.md |
Tier 2: Structural Patterns
| Pattern | One-Line | When to Use | Reference |
|---|---|---|---|
| Adapter | Convert interfaces | Integrate incompatible code | adapter.md |
| Facade | Simplify complex subsystems | Hide complexity behind simple API | facade.md |
| Composite | Uniform tree structures | Part-whole hierarchies | composite.md |
| Proxy | Control access to objects | Lazy load, access control, caching | proxy.md |
Tier 3: Enterprise/Architectural Patterns
| Pattern | One-Line | When to Use | Reference |
|---|---|---|---|
| Repository | Collection-like data access | Decouple domain from data layer | repository.md |
| Unit of Work | Coordinate atomic changes | Transaction management | unit-of-work.md |
| Service Layer | Orchestrate business operations | Define application boundary | service-layer.md |
| DTO | Shape data for transfer | API contracts, prevent over-exposure | dto.md |
Additional Important Patterns
| Pattern | One-Line | When to Use | Reference |
|---|---|---|---|
| Builder | Step-by-step object construction | Complex objects, fluent APIs | builder.md |
| State | Behavior changes with state | State machines, workflow | state.md |
| Template Method | Algorithm skeleton with hooks | Framework extension points | template-method.md |
| Chain of Responsibility | Pass request along handlers | Middleware, pipelines | chain-of-responsibility.md |
| Mediator | Centralize complex communication | Reduce component coupling | mediator.md |
| Lazy Load | Defer expensive loading | Performance, large object graphs | lazy-load.md |
| Identity Map | Ensure object identity | ORM, prevent duplicates | identity-map.md |
Common Mistakes
| Mistake | Symptom | Fix |
|---|---|---|
| Pattern Overuse | Simple operations require navigating many classes | Only use when solving real problem |
| Wrong Pattern | Code feels forced, awkward | Re-examine actual problem |
| Inheritance Abuse | Deep hierarchies, fragile base class | Favor composition (Strategy, Decorator) |
| Singleton Abuse | Global state, hidden dependencies, hard to test | Use dependency injection instead |
| Premature Abstraction | Interfaces with single implementation | Wait for real need to vary |
Anti-Patterns to Recognize
- God Object: One class does everything → Split using SRP
- Anemic Domain Model: Objects are just data bags → Move behavior to objects
- Golden Hammer: Same pattern everywhere → Match pattern to problem
- Lava Flow: Dead code nobody removes → Delete it, VCS has your back
Modern Variations
| Modern Pattern | Based On | Description |
|---|---|---|
| Dependency Injection | Strategy + Factory | Container creates and injects dependencies |
| Middleware | Decorator + Chain of Responsibility | Request/response pipeline |
| Event Sourcing | Command | Store state changes as events |
| CQRS | Command/Query separation | Separate read/write models |
| Hooks (React/Vue) | Observer + Strategy | Functional lifecycle subscriptions |
Implementation Checklist
Before implementing a pattern:
- [ ] Pattern solves a real problem in this codebase
- [ ] Considered simpler alternatives
- [ ] Trade-offs acceptable for this context
- [ ] Team understands the pattern
- [ ] Won't over-engineer the solution
Adapter Pattern
Intent
Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.
The Problem
You need to use an existing class, but its interface doesn't match what your code expects:
- Integrating third-party libraries
- Working with legacy systems
- Using APIs with different conventions
- Swapping implementations without changing client code
Real Scenario
// Your application expects this interface
interface PaymentGateway {
charge(amount: number, currency: string, cardToken: string): Promise<PaymentResult>;
refund(transactionId: string, amount: number): Promise<RefundResult>;
}
// But Stripe's SDK has a different interface
class Stripe {
paymentIntents = {
create: async (params: {
amount: number;
currency: string;
payment_method: string;
confirm: boolean;
}) => { /* ... */ }
};
refunds = {
create: async (params: {
payment_intent: string;
amount?: number;
}) => { /* ... */ }
};
}
// And PayPal has yet another interface
class PayPalClient {
async createOrder(body: { purchase_units: Array<{ amount: { value: string } }> }) { /* ... */ }
async captureOrder(orderId: string) { /* ... */ }
async refundCapture(captureId: string, body: { amount: { value: string } }) { /* ... */ }
}The Solution
Create adapters that convert each external interface to your expected interface:
// Your application's interface
interface PaymentGateway {
charge(amount: number, currency: string, cardToken: string): Promise<PaymentResult>;
refund(transactionId: string, amount: number): Promise<RefundResult>;
}
interface PaymentResult {
success: boolean;
transactionId: string;
error?: string;
}
interface RefundResult {
success: boolean;
refundId: string;
error?: string;
}
// Stripe Adapter
class StripeAdapter implements PaymentGateway {
private stripe: Stripe;
constructor(apiKey: string) {
this.stripe = new Stripe(apiKey);
}
async charge(amount: number, currency: string, cardToken: string): Promise<PaymentResult> {
try {
const paymentIntent = await this.stripe.paymentIntents.create({
amount: Math.round(amount * 100), // Stripe uses cents
currency: currency.toLowerCase(),
payment_method: cardToken,
confirm: true
});
return {
success: paymentIntent.status === 'succeeded',
transactionId: paymentIntent.id
};
} catch (error) {
return {
success: false,
transactionId: '',
error: (error as Error).message
};
}
}
async refund(transactionId: string, amount: number): Promise<RefundResult> {
try {
const refund = await this.stripe.refunds.create({
payment_intent: transactionId,
amount: Math.round(amount * 100)
});
return {
success: refund.status === 'succeeded',
refundId: refund.id
};
} catch (error) {
return {
success: false,
refundId: '',
error: (error as Error).message
};
}
}
}
// PayPal Adapter
class PayPalAdapter implements PaymentGateway {
private client: PayPalClient;
private captureIds: Map<string, string> = new Map(); // orderId -> captureId
constructor(clientId: string, clientSecret: string) {
this.client = new PayPalClient(clientId, clientSecret);
}
async charge(amount: number, currency: string, _cardToken: string): Promise<PaymentResult> {
try {
// PayPal flow is different - create order then capture
const order = await this.client.createOrder({
purchase_units: [{
amount: {
value: amount.toFixed(2),
currency_code: currency.toUpperCase()
}
}]
});
const capture = await this.client.captureOrder(order.id);
const captureId = capture.purchase_units[0].payments.captures[0].id;
// Store mapping for refunds
this.captureIds.set(order.id, captureId);
return {
success: capture.status === 'COMPLETED',
transactionId: order.id
};
} catch (error) {
return {
success: false,
transactionId: '',
error: (error as Error).message
};
}
}
async refund(transactionId: string, amount: number): Promise<RefundResult> {
try {
const captureId = this.captureIds.get(transactionId);
if (!captureId) {
throw new Error('Capture ID not found for transaction');
}
const refund = await this.client.refundCapture(captureId, {
amount: { value: amount.toFixed(2) }
});
return {
success: refund.status === 'COMPLETED',
refundId: refund.id
};
} catch (error) {
return {
success: false,
refundId: '',
error: (error as Error).message
};
}
}
}
// Usage - client code doesn't know or care which provider
class CheckoutService {
constructor(private paymentGateway: PaymentGateway) {}
async processPayment(cart: Cart, paymentMethod: string): Promise<Order> {
const result = await this.paymentGateway.charge(
cart.total,
'USD',
paymentMethod
);
if (!result.success) {
throw new PaymentError(result.error);
}
return this.createOrder(cart, result.transactionId);
}
}
// Configuration determines which adapter
const paymentGateway = process.env.PAYMENT_PROVIDER === 'stripe'
? new StripeAdapter(process.env.STRIPE_KEY!)
: new PayPalAdapter(process.env.PAYPAL_ID!, process.env.PAYPAL_SECRET!);
const checkoutService = new CheckoutService(paymentGateway);Structure
┌─────────────────┐ ┌─────────────────┐
│ Client │────────▶│ Target │
└─────────────────┘ │ <<interface>> │
│ + request() │
└────────┬────────┘
△
│
┌────────┴────────┐
│ Adapter │
│ │
│ - adaptee │──────┐
│ + request() │ │
└─────────────────┘ │
▼
┌─────────────────┐
│ Adaptee │
│ │
│ + specificReq() │
└─────────────────┘JavaScript/TypeScript Patterns
Function Adapter
// Old callback-based API
function readFileCallback(
path: string,
callback: (err: Error | null, data: string | null) => void
): void {
fs.readFile(path, 'utf-8', callback);
}
// Adapt to Promise-based
function adaptToPromise<T>(
callbackFn: (callback: (err: Error | null, result: T | null) => void) => void
): Promise<T> {
return new Promise((resolve, reject) => {
callbackFn((err, result) => {
if (err) reject(err);
else resolve(result!);
});
});
}
// Usage
const readFileAsync = (path: string) =>
adaptToPromise<string>((cb) => readFileCallback(path, cb));
// Generic promisify adapter
function promisify<T>(
fn: (...args: [...any[], (err: Error | null, result: T) => void]) => void
): (...args: any[]) => Promise<T> {
return (...args: any[]) => {
return new Promise((resolve, reject) => {
fn(...args, (err: Error | null, result: T) => {
if (err) reject(err);
else resolve(result);
});
});
};
}
const readFile = promisify(fs.readFile);
const data = await readFile('file.txt', 'utf-8');Class Adapter (using inheritance)
// Legacy logger with different interface
class LegacyLogger {
writeLog(level: number, msg: string): void {
const levels = ['DEBUG', 'INFO', 'WARN', 'ERROR'];
console.log(`[${levels[level]}] ${msg}`);
}
}
// Modern interface
interface Logger {
debug(message: string): void;
info(message: string): void;
warn(message: string): void;
error(message: string): void;
}
// Class adapter - extends adaptee, implements target
class LoggerAdapter extends LegacyLogger implements Logger {
debug(message: string): void {
this.writeLog(0, message);
}
info(message: string): void {
this.writeLog(1, message);
}
warn(message: string): void {
this.writeLog(2, message);
}
error(message: string): void {
this.writeLog(3, message);
}
}Object Adapter (using composition - preferred)
// Same scenario but using composition
class LoggerAdapter implements Logger {
constructor(private legacyLogger: LegacyLogger) {}
debug(message: string): void {
this.legacyLogger.writeLog(0, message);
}
info(message: string): void {
this.legacyLogger.writeLog(1, message);
}
warn(message: string): void {
this.legacyLogger.writeLog(2, message);
}
error(message: string): void {
this.legacyLogger.writeLog(3, message);
}
}
// Usage
const legacyLogger = new LegacyLogger();
const logger: Logger = new LoggerAdapter(legacyLogger);
logger.info('This works with modern interface');Real-World Applications
1. Database Driver Adapter
// Your application's interface
interface Database {
query<T>(sql: string, params?: any[]): Promise<T[]>;
execute(sql: string, params?: any[]): Promise<{ affectedRows: number }>;
beginTransaction(): Promise<Transaction>;
}
interface Transaction {
query<T>(sql: string, params?: any[]): Promise<T[]>;
execute(sql: string, params?: any[]): Promise<{ affectedRows: number }>;
commit(): Promise<void>;
rollback(): Promise<void>;
}
// PostgreSQL Adapter (using pg library)
import { Pool, PoolClient } from 'pg';
class PostgresAdapter implements Database {
private pool: Pool;
constructor(connectionString: string) {
this.pool = new Pool({ connectionString });
}
async query<T>(sql: string, params?: any[]): Promise<T[]> {
const result = await this.pool.query(sql, params);
return result.rows as T[];
}
async execute(sql: string, params?: any[]): Promise<{ affectedRows: number }> {
const result = await this.pool.query(sql, params);
return { affectedRows: result.rowCount ?? 0 };
}
async beginTransaction(): Promise<Transaction> {
const client = await this.pool.connect();
await client.query('BEGIN');
return new PostgresTransaction(client);
}
}
class PostgresTransaction implements Transaction {
constructor(private client: PoolClient) {}
async query<T>(sql: string, params?: any[]): Promise<T[]> {
const result = await this.client.query(sql, params);
return result.rows as T[];
}
async execute(sql: string, params?: any[]): Promise<{ affectedRows: number }> {
const result = await this.client.query(sql, params);
return { affectedRows: result.rowCount ?? 0 };
}
async commit(): Promise<void> {
await this.client.query('COMMIT');
this.client.release();
}
async rollback(): Promise<void> {
await this.client.query('ROLLBACK');
this.client.release();
}
}
// MySQL Adapter (using mysql2)
import mysql from 'mysql2/promise';
class MySQLAdapter implements Database {
private pool: mysql.Pool;
constructor(config: mysql.PoolOptions) {
this.pool = mysql.createPool(config);
}
async query<T>(sql: string, params?: any[]): Promise<T[]> {
const [rows] = await this.pool.execute(sql, params);
return rows as T[];
}
async execute(sql: string, params?: any[]): Promise<{ affectedRows: number }> {
const [result] = await this.pool.execute(sql, params) as [mysql.ResultSetHeader, any];
return { affectedRows: result.affectedRows };
}
async beginTransaction(): Promise<Transaction> {
const connection = await this.pool.getConnection();
await connection.beginTransaction();
return new MySQLTransaction(connection);
}
}
// Usage - swap databases without changing business logic
const db: Database = process.env.DB_TYPE === 'postgres'
? new PostgresAdapter(process.env.POSTGRES_URL!)
: new MySQLAdapter({ uri: process.env.MYSQL_URL });
const users = await db.query<User>('SELECT * FROM users WHERE active = ?', [true]);2. HTTP Client Adapter
// Your application's HTTP interface
interface HttpClient {
get<T>(url: string, config?: RequestConfig): Promise<T>;
post<T>(url: string, data?: unknown, config?: RequestConfig): Promise<T>;
put<T>(url: string, data?: unknown, config?: RequestConfig): Promise<T>;
delete<T>(url: string, config?: RequestConfig): Promise<T>;
}
interface RequestConfig {
headers?: Record<string, string>;
params?: Record<string, string>;
timeout?: number;
}
// Fetch API Adapter
class FetchAdapter implements HttpClient {
constructor(private baseURL: string = '') {}
private async request<T>(
method: string,
url: string,
data?: unknown,
config?: RequestConfig
): Promise<T> {
const fullUrl = new URL(url, this.baseURL);
if (config?.params) {
Object.entries(config.params).forEach(([key, value]) => {
fullUrl.searchParams.append(key, value);
});
}
const controller = new AbortController();
const timeoutId = config?.timeout
? setTimeout(() => controller.abort(), config.timeout)
: null;
try {
const response = await fetch(fullUrl.toString(), {
method,
headers: {
'Content-Type': 'application/json',
...config?.headers
},
body: data ? JSON.stringify(data) : undefined,
signal: controller.signal
});
if (!response.ok) {
throw new HttpError(response.status, await response.text());
}
return response.json();
} finally {
if (timeoutId) clearTimeout(timeoutId);
}
}
get<T>(url: string, config?: RequestConfig): Promise<T> {
return this.request<T>('GET', url, undefined, config);
}
post<T>(url: string, data?: unknown, config?: RequestConfig): Promise<T> {
return this.request<T>('POST', url, data, config);
}
put<T>(url: string, data?: unknown, config?: RequestConfig): Promise<T> {
return this.request<T>('PUT', url, data, config);
}
delete<T>(url: string, config?: RequestConfig): Promise<T> {
return this.request<T>('DELETE', url, undefined, config);
}
}
// Axios Adapter
import axios, { AxiosInstance } from 'axios';
class AxiosAdapter implements HttpClient {
private client: AxiosInstance;
constructor(baseURL: string = '') {
this.client = axios.create({ baseURL });
}
async get<T>(url: string, config?: RequestConfig): Promise<T> {
const response = await this.client.get<T>(url, {
headers: config?.headers,
params: config?.params,
timeout: config?.timeout
});
return response.data;
}
async post<T>(url: string, data?: unknown, config?: RequestConfig): Promise<T> {
const response = await this.client.post<T>(url, data, {
headers: config?.headers,
timeout: config?.timeout
});
return response.data;
}
async put<T>(url: string, data?: unknown, config?: RequestConfig): Promise<T> {
const response = await this.client.put<T>(url, data, {
headers: config?.headers,
timeout: config?.timeout
});
return response.data;
}
async delete<T>(url: string, config?: RequestConfig): Promise<T> {
const response = await this.client.delete<T>(url, {
headers: config?.headers,
timeout: config?.timeout
});
return response.data;
}
}
// Usage
const httpClient: HttpClient = process.env.USE_AXIOS
? new AxiosAdapter('https://api.example.com')
: new FetchAdapter('https://api.example.com');3. Event System Adapter
// Your application's event interface
interface EventBus {
on<T>(event: string, handler: (data: T) => void): () => void;
emit<T>(event: string, data: T): void;
once<T>(event: string, handler: (data: T) => void): () => void;
}
// Adapt Node.js EventEmitter
import { EventEmitter } from 'events';
class NodeEventAdapter implements EventBus {
private emitter = new EventEmitter();
on<T>(event: string, handler: (data: T) => void): () => void {
this.emitter.on(event, handler);
return () => this.emitter.off(event, handler);
}
emit<T>(event: string, data: T): void {
this.emitter.emit(event, data);
}
once<T>(event: string, handler: (data: T) => void): () => void {
this.emitter.once(event, handler);
return () => this.emitter.off(event, handler);
}
}
// Adapt browser's window events
class BrowserEventAdapter implements EventBus {
private handlers = new Map<string, Map<Function, EventListener>>();
on<T>(event: string, handler: (data: T) => void): () => void {
const listener = ((e: CustomEvent<T>) => handler(e.detail)) as EventListener;
if (!this.handlers.has(event)) {
this.handlers.set(event, new Map());
}
this.handlers.get(event)!.set(handler, listener);
window.addEventListener(event, listener);
return () => {
window.removeEventListener(event, listener);
this.handlers.get(event)?.delete(handler);
};
}
emit<T>(event: string, data: T): void {
window.dispatchEvent(new CustomEvent(event, { detail: data }));
}
once<T>(event: string, handler: (data: T) => void): () => void {
const unsubscribe = this.on<T>(event, (data) => {
unsubscribe();
handler(data);
});
return unsubscribe;
}
}
// Adapt Redis Pub/Sub
import Redis from 'ioredis';
class RedisEventAdapter implements EventBus {
private publisher: Redis;
private subscriber: Redis;
private handlers = new Map<string, Set<Function>>();
constructor(redisUrl: string) {
this.publisher = new Redis(redisUrl);
this.subscriber = new Redis(redisUrl);
this.subscriber.on('message', (channel, message) => {
const handlers = this.handlers.get(channel);
if (handlers) {
const data = JSON.parse(message);
handlers.forEach(handler => handler(data));
}
});
}
on<T>(event: string, handler: (data: T) => void): () => void {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
this.subscriber.subscribe(event);
}
this.handlers.get(event)!.add(handler);
return () => {
const handlers = this.handlers.get(event);
if (handlers) {
handlers.delete(handler);
if (handlers.size === 0) {
this.handlers.delete(event);
this.subscriber.unsubscribe(event);
}
}
};
}
emit<T>(event: string, data: T): void {
this.publisher.publish(event, JSON.stringify(data));
}
once<T>(event: string, handler: (data: T) => void): () => void {
const unsubscribe = this.on<T>(event, (data) => {
unsubscribe();
handler(data);
});
return unsubscribe;
}
}When to Use
Use Adapter when:
- You want to use an existing class with an incompatible interface
- You need to create a reusable class that cooperates with unrelated classes
- You're integrating third-party libraries
- You need to swap implementations without changing client code
Don't use Adapter when:
- The interfaces are similar enough to not need adaptation
- You can modify the source class directly
- The adaptation is trivial and doesn't justify a new class
Related Patterns
- Bridge: Separates abstraction from implementation upfront; Adapter retrofits
- Decorator: Enhances without changing interface; Adapter changes interface
- Facade: Defines a new simpler interface; Adapter makes existing interface usable
- Proxy: Same interface; Adapter different interface
Builder Pattern
Intent
Separate the construction of a complex object from its representation, allowing the same construction process to create different representations.
The Problem
Complex object construction with many optional parameters:
- Constructor with 10+ parameters
- Unclear which parameters are required
- Boolean parameters with unclear meaning
- Can't validate until all parameters set
// Without Builder - constructor hell
const email = new Email(
'Welcome!', // subject
'Hello, welcome to our app', // body
'user@example.com', // to
'noreply@app.com', // from
null, // cc - what does null mean?
null, // bcc
true, // isHtml - which true is this?
false, // requestReceipt
'high', // priority
[], // attachments
null, // replyTo
{ 'X-Campaign': 'welcome' } // headers
);
// Impossible to read, easy to get wrongThe Solution
Create a builder with fluent interface:
// Email built with fluent builder
const email = new EmailBuilder()
.to('user@example.com')
.from('noreply@app.com')
.subject('Welcome!')
.htmlBody('<h1>Hello!</h1><p>Welcome to our app.</p>')
.priority('high')
.header('X-Campaign', 'welcome')
.build();Implementation
interface Email {
to: string[];
cc: string[];
bcc: string[];
from: string;
replyTo: string | null;
subject: string;
body: string;
isHtml: boolean;
priority: 'low' | 'normal' | 'high';
attachments: Attachment[];
headers: Record<string, string>;
}
class EmailBuilder {
private email: Partial<Email> = {
to: [],
cc: [],
bcc: [],
isHtml: false,
priority: 'normal',
attachments: [],
headers: {}
};
to(...addresses: string[]): this {
this.email.to = [...this.email.to!, ...addresses];
return this;
}
cc(...addresses: string[]): this {
this.email.cc = [...this.email.cc!, ...addresses];
return this;
}
bcc(...addresses: string[]): this {
this.email.bcc = [...this.email.bcc!, ...addresses];
return this;
}
from(address: string): this {
this.email.from = address;
return this;
}
replyTo(address: string): this {
this.email.replyTo = address;
return this;
}
subject(text: string): this {
this.email.subject = text;
return this;
}
textBody(text: string): this {
this.email.body = text;
this.email.isHtml = false;
return this;
}
htmlBody(html: string): this {
this.email.body = html;
this.email.isHtml = true;
return this;
}
priority(level: 'low' | 'normal' | 'high'): this {
this.email.priority = level;
return this;
}
attach(attachment: Attachment): this {
this.email.attachments!.push(attachment);
return this;
}
header(name: string, value: string): this {
this.email.headers![name] = value;
return this;
}
build(): Email {
// Validate required fields
if (!this.email.to?.length) {
throw new Error('Email must have at least one recipient');
}
if (!this.email.from) {
throw new Error('Email must have a sender');
}
if (!this.email.subject) {
throw new Error('Email must have a subject');
}
if (!this.email.body) {
throw new Error('Email must have a body');
}
return this.email as Email;
}
}Query Builder
Common use in database queries:
class QueryBuilder {
private query: {
select: string[];
from: string;
joins: string[];
where: string[];
orderBy: string[];
limit: number | null;
offset: number | null;
params: any[];
};
constructor(table: string) {
this.query = {
select: ['*'],
from: table,
joins: [],
where: [],
orderBy: [],
limit: null,
offset: null,
params: []
};
}
select(...columns: string[]): this {
this.query.select = columns;
return this;
}
join(table: string, condition: string): this {
this.query.joins.push(`JOIN ${table} ON ${condition}`);
return this;
}
leftJoin(table: string, condition: string): this {
this.query.joins.push(`LEFT JOIN ${table} ON ${condition}`);
return this;
}
where(condition: string, ...params: any[]): this {
this.query.where.push(condition);
this.query.params.push(...params);
return this;
}
andWhere(condition: string, ...params: any[]): this {
return this.where(condition, ...params);
}
orWhere(condition: string, ...params: any[]): this {
if (this.query.where.length > 0) {
const last = this.query.where.pop()!;
this.query.where.push(`(${last} OR ${condition})`);
} else {
this.query.where.push(condition);
}
this.query.params.push(...params);
return this;
}
orderBy(column: string, direction: 'ASC' | 'DESC' = 'ASC'): this {
this.query.orderBy.push(`${column} ${direction}`);
return this;
}
limit(count: number): this {
this.query.limit = count;
return this;
}
offset(count: number): this {
this.query.offset = count;
return this;
}
toSQL(): { sql: string; params: any[] } {
let sql = `SELECT ${this.query.select.join(', ')} FROM ${this.query.from}`;
if (this.query.joins.length > 0) {
sql += ` ${this.query.joins.join(' ')}`;
}
if (this.query.where.length > 0) {
sql += ` WHERE ${this.query.where.join(' AND ')}`;
}
if (this.query.orderBy.length > 0) {
sql += ` ORDER BY ${this.query.orderBy.join(', ')}`;
}
if (this.query.limit !== null) {
sql += ` LIMIT ${this.query.limit}`;
}
if (this.query.offset !== null) {
sql += ` OFFSET ${this.query.offset}`;
}
return { sql, params: this.query.params };
}
async execute<T>(db: Database): Promise<T[]> {
const { sql, params } = this.toSQL();
return db.query<T>(sql, params);
}
}
// Usage
const users = await new QueryBuilder('users')
.select('users.id', 'users.name', 'orders.total')
.leftJoin('orders', 'orders.user_id = users.id')
.where('users.active = $1', true)
.andWhere('users.created_at > $2', lastMonth)
.orderBy('users.name', 'ASC')
.limit(20)
.execute<UserWithOrders>(db);Request Builder (HTTP Client)
class RequestBuilder {
private config: RequestConfig = {
method: 'GET',
headers: {},
timeout: 30000
};
constructor(private baseUrl: string) {}
get(path: string): this {
this.config.method = 'GET';
this.config.path = path;
return this;
}
post(path: string): this {
this.config.method = 'POST';
this.config.path = path;
return this;
}
put(path: string): this {
this.config.method = 'PUT';
this.config.path = path;
return this;
}
delete(path: string): this {
this.config.method = 'DELETE';
this.config.path = path;
return this;
}
header(name: string, value: string): this {
this.config.headers[name] = value;
return this;
}
bearerToken(token: string): this {
return this.header('Authorization', `Bearer ${token}`);
}
contentType(type: string): this {
return this.header('Content-Type', type);
}
json(data: any): this {
this.config.body = JSON.stringify(data);
return this.contentType('application/json');
}
formData(data: Record<string, string>): this {
const form = new URLSearchParams(data);
this.config.body = form.toString();
return this.contentType('application/x-www-form-urlencoded');
}
query(params: Record<string, string | number>): this {
this.config.queryParams = params;
return this;
}
timeout(ms: number): this {
this.config.timeout = ms;
return this;
}
async send<T>(): Promise<T> {
let url = `${this.baseUrl}${this.config.path}`;
if (this.config.queryParams) {
const params = new URLSearchParams(
Object.entries(this.config.queryParams).map(([k, v]) => [k, String(v)])
);
url += `?${params}`;
}
const response = await fetch(url, {
method: this.config.method,
headers: this.config.headers,
body: this.config.body,
signal: AbortSignal.timeout(this.config.timeout)
});
if (!response.ok) {
throw new ApiError(response.status, await response.text());
}
return response.json();
}
}
// Usage
const api = new RequestBuilder('https://api.example.com');
const user = await api
.post('/users')
.bearerToken(token)
.json({ name: 'John', email: 'john@example.com' })
.send<User>();
const products = await api
.get('/products')
.query({ category: 'electronics', limit: 10 })
.send<Product[]>();Test Data Builder
class UserBuilder {
private user: Partial<User> = {
id: generateId(),
email: 'test@example.com',
name: 'Test User',
role: 'member',
verified: true,
createdAt: new Date()
};
withId(id: string): this {
this.user.id = id;
return this;
}
withEmail(email: string): this {
this.user.email = email;
return this;
}
withName(name: string): this {
this.user.name = name;
return this;
}
asAdmin(): this {
this.user.role = 'admin';
return this;
}
asMember(): this {
this.user.role = 'member';
return this;
}
unverified(): this {
this.user.verified = false;
return this;
}
createdDaysAgo(days: number): this {
const date = new Date();
date.setDate(date.getDate() - days);
this.user.createdAt = date;
return this;
}
build(): User {
return new User(
this.user.id!,
this.user.email!,
this.user.name!,
this.user.role!,
this.user.verified!,
this.user.createdAt!
);
}
}
// Usage in tests
describe('UserService', () => {
it('should allow admin to delete users', async () => {
const admin = new UserBuilder().asAdmin().build();
const target = new UserBuilder().asMember().build();
await userService.deleteUser(admin.id, target.id);
expect(await userRepository.findById(target.id)).toBeNull();
});
it('should reject unverified users', async () => {
const user = new UserBuilder().unverified().build();
await expect(
orderService.placeOrder(user.id, items)
).rejects.toThrow('User not verified');
});
});Director Pattern
When same build steps are reused:
class EmailDirector {
constructor(private builder: EmailBuilder) {}
createWelcomeEmail(user: User): Email {
return this.builder
.to(user.email)
.from('welcome@app.com')
.subject('Welcome to Our App!')
.htmlBody(welcomeTemplate(user.name))
.priority('normal')
.header('X-Campaign', 'welcome')
.build();
}
createPasswordResetEmail(user: User, resetLink: string): Email {
return this.builder
.to(user.email)
.from('security@app.com')
.subject('Password Reset Request')
.htmlBody(passwordResetTemplate(user.name, resetLink))
.priority('high')
.header('X-Campaign', 'password-reset')
.build();
}
createOrderConfirmation(user: User, order: Order): Email {
return this.builder
.to(user.email)
.from('orders@app.com')
.subject(`Order Confirmation #${order.id}`)
.htmlBody(orderConfirmationTemplate(order))
.attach(generateInvoicePdf(order))
.build();
}
}
// Usage
const director = new EmailDirector(new EmailBuilder());
const welcomeEmail = director.createWelcomeEmail(user);
await emailService.send(welcomeEmail);When to Use
Use Builder when:
- Object has many optional parameters
- Construction requires multiple steps
- You want readable, self-documenting code
- Same construction process for different representations
Don't use Builder when:
- Object is simple with few required parameters
- No optional parameters or variations
- Object can be created in one step
Related Patterns
- Factory: Creates object in one step; Builder is multi-step
- Prototype: Clones existing objects instead of building
- Fluent Interface: Often used with Builder for chaining
Chain of Responsibility Pattern
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 an object handles it.
The Problem
Hardcoded handler logic:
- Sender must know which handler to call
- Adding new handlers requires modifying sender
- Complex if/else or switch chains
- Tight coupling between request and handler
// Without Chain - hardcoded handling
class SupportTicketHandler {
handleTicket(ticket: Ticket): void {
if (ticket.type === 'billing') {
// Handle billing
this.billingDepartment.handle(ticket);
} else if (ticket.type === 'technical' && ticket.severity === 'critical') {
// Handle critical tech issues
this.seniorEngineer.handle(ticket);
} else if (ticket.type === 'technical') {
// Handle regular tech issues
this.techSupport.handle(ticket);
} else if (ticket.type === 'general') {
// Handle general inquiries
this.customerService.handle(ticket);
} else {
// Fallback
this.manager.handle(ticket);
}
// Adding new type requires modifying this class
}
}The Solution
Create a chain of handlers, each deciding whether to process or pass along:
// Handler interface
interface Handler<T> {
setNext(handler: Handler<T>): Handler<T>;
handle(request: T): void;
}
// Base handler with chaining logic
abstract class BaseHandler<T> implements Handler<T> {
private nextHandler: Handler<T> | null = null;
setNext(handler: Handler<T>): Handler<T> {
this.nextHandler = handler;
return handler;
}
handle(request: T): void {
if (this.canHandle(request)) {
this.process(request);
} else if (this.nextHandler) {
this.nextHandler.handle(request);
} else {
this.handleUnprocessed(request);
}
}
protected abstract canHandle(request: T): boolean;
protected abstract process(request: T): void;
protected handleUnprocessed(request: T): void {
console.log('No handler found for request');
}
}
// Concrete handlers
class BillingHandler extends BaseHandler<Ticket> {
protected canHandle(ticket: Ticket): boolean {
return ticket.type === 'billing';
}
protected process(ticket: Ticket): void {
console.log(`Billing department handling ticket: ${ticket.id}`);
// Process billing issue
}
}
class CriticalTechHandler extends BaseHandler<Ticket> {
protected canHandle(ticket: Ticket): boolean {
return ticket.type === 'technical' && ticket.severity === 'critical';
}
protected process(ticket: Ticket): void {
console.log(`Senior engineer handling critical ticket: ${ticket.id}`);
// Escalate to senior engineer
}
}
class TechSupportHandler extends BaseHandler<Ticket> {
protected canHandle(ticket: Ticket): boolean {
return ticket.type === 'technical';
}
protected process(ticket: Ticket): void {
console.log(`Tech support handling ticket: ${ticket.id}`);
// Handle technical issue
}
}
class GeneralSupportHandler extends BaseHandler<Ticket> {
protected canHandle(ticket: Ticket): boolean {
return ticket.type === 'general';
}
protected process(ticket: Ticket): void {
console.log(`Customer service handling ticket: ${ticket.id}`);
// Handle general inquiry
}
}
class ManagerHandler extends BaseHandler<Ticket> {
protected canHandle(ticket: Ticket): boolean {
return true; // Handles everything that falls through
}
protected process(ticket: Ticket): void {
console.log(`Manager handling unhandled ticket: ${ticket.id}`);
// Escalate to manager
}
}
// Build the chain
const billing = new BillingHandler();
const criticalTech = new CriticalTechHandler();
const techSupport = new TechSupportHandler();
const general = new GeneralSupportHandler();
const manager = new ManagerHandler();
billing
.setNext(criticalTech)
.setNext(techSupport)
.setNext(general)
.setNext(manager);
// Use the chain
billing.handle({ id: '1', type: 'billing', severity: 'normal' });
billing.handle({ id: '2', type: 'technical', severity: 'critical' });
billing.handle({ id: '3', type: 'unknown', severity: 'normal' });Middleware Pattern (Express-style)
type NextFunction = () => void;
type Middleware = (req: Request, res: Response, next: NextFunction) => void;
class MiddlewareChain {
private middlewares: Middleware[] = [];
use(middleware: Middleware): this {
this.middlewares.push(middleware);
return this;
}
handle(req: Request, res: Response): void {
let index = 0;
const next = (): void => {
if (index < this.middlewares.length) {
const middleware = this.middlewares[index++];
middleware(req, res, next);
}
};
next();
}
}
// Middleware implementations
const logging: Middleware = (req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
};
const authentication: Middleware = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
res.status(401).json({ error: 'Unauthorized' });
return; // Don't call next - stop chain
}
req.user = verifyToken(token);
next();
};
const authorization: Middleware = (req, res, next) => {
if (!req.user.hasPermission(req.requiredPermission)) {
res.status(403).json({ error: 'Forbidden' });
return;
}
next();
};
const errorHandler: Middleware = (req, res, next) => {
try {
next();
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
};
// Build middleware chain
const chain = new MiddlewareChain()
.use(logging)
.use(authentication)
.use(authorization)
.use(errorHandler);Validation Chain
interface ValidationResult {
valid: boolean;
errors: string[];
}
interface Validator<T> {
setNext(validator: Validator<T>): Validator<T>;
validate(data: T): ValidationResult;
}
abstract class BaseValidator<T> implements Validator<T> {
private next: Validator<T> | null = null;
setNext(validator: Validator<T>): Validator<T> {
this.next = validator;
return validator;
}
validate(data: T): ValidationResult {
const result = this.check(data);
if (!result.valid) {
return result;
}
if (this.next) {
return this.next.validate(data);
}
return { valid: true, errors: [] };
}
protected abstract check(data: T): ValidationResult;
}
interface UserData {
email: string;
password: string;
age: number;
}
class EmailValidator extends BaseValidator<UserData> {
protected check(data: UserData): ValidationResult {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(data.email)) {
return { valid: false, errors: ['Invalid email format'] };
}
return { valid: true, errors: [] };
}
}
class PasswordValidator extends BaseValidator<UserData> {
protected check(data: UserData): ValidationResult {
const errors: string[] = [];
if (data.password.length < 8) {
errors.push('Password must be at least 8 characters');
}
if (!/[A-Z]/.test(data.password)) {
errors.push('Password must contain uppercase letter');
}
if (!/[0-9]/.test(data.password)) {
errors.push('Password must contain a number');
}
return {
valid: errors.length === 0,
errors
};
}
}
class AgeValidator extends BaseValidator<UserData> {
protected check(data: UserData): ValidationResult {
if (data.age < 18) {
return { valid: false, errors: ['Must be at least 18 years old'] };
}
if (data.age > 120) {
return { valid: false, errors: ['Invalid age'] };
}
return { valid: true, errors: [] };
}
}
// Build validation chain
const validator = new EmailValidator();
validator
.setNext(new PasswordValidator())
.setNext(new AgeValidator());
// Validate
const result = validator.validate({
email: 'test@example.com',
password: 'weak',
age: 25
});
// { valid: false, errors: ['Password must be at least 8 characters', ...] }Event Bubbling
interface UIComponent {
parent: UIComponent | null;
handleEvent(event: UIEvent): boolean;
}
class BaseComponent implements UIComponent {
parent: UIComponent | null = null;
private eventHandlers: Map<string, (event: UIEvent) => boolean> = new Map();
on(eventType: string, handler: (event: UIEvent) => boolean): void {
this.eventHandlers.set(eventType, handler);
}
handleEvent(event: UIEvent): boolean {
const handler = this.eventHandlers.get(event.type);
if (handler) {
const handled = handler(event);
if (handled) {
return true; // Event consumed, stop bubbling
}
}
// Bubble to parent
if (this.parent) {
return this.parent.handleEvent(event);
}
return false;
}
}
class Button extends BaseComponent {
constructor(private label: string) {
super();
}
}
class Panel extends BaseComponent {
private children: UIComponent[] = [];
addChild(child: UIComponent): void {
child.parent = this;
this.children.push(child);
}
}
class Window extends BaseComponent {
private panels: Panel[] = [];
addPanel(panel: Panel): void {
panel.parent = this;
this.panels.push(panel);
}
}
// Usage
const window = new Window();
window.on('click', (e) => {
console.log('Window caught unhandled click');
return true;
});
const panel = new Panel();
panel.on('click', (e) => {
if (e.target === 'close') {
console.log('Panel closing');
return true;
}
return false; // Let it bubble
});
const button = new Button('Submit');
button.on('click', (e) => {
console.log('Button clicked');
return false; // Let it bubble to panel
});
window.addPanel(panel);
panel.addChild(button);
// Event bubbles: button -> panel -> window
button.handleEvent({ type: 'click', target: 'submit' });Approval Workflow
interface PurchaseRequest {
id: string;
amount: number;
requester: string;
description: string;
approvals: Approval[];
}
interface Approval {
approver: string;
approved: boolean;
comment?: string;
timestamp: Date;
}
abstract class Approver {
protected successor: Approver | null = null;
protected approvalLimit: number;
protected name: string;
constructor(name: string, limit: number) {
this.name = name;
this.approvalLimit = limit;
}
setSuccessor(approver: Approver): Approver {
this.successor = approver;
return approver;
}
async process(request: PurchaseRequest): Promise<PurchaseRequest> {
if (request.amount <= this.approvalLimit) {
return this.approve(request);
} else if (this.successor) {
// Add partial approval and pass up the chain
request.approvals.push({
approver: this.name,
approved: true,
comment: 'Approved, escalating for final approval',
timestamp: new Date()
});
return this.successor.process(request);
} else {
return this.reject(request, 'Exceeds maximum approval limit');
}
}
protected approve(request: PurchaseRequest): PurchaseRequest {
request.approvals.push({
approver: this.name,
approved: true,
timestamp: new Date()
});
return request;
}
protected reject(request: PurchaseRequest, reason: string): PurchaseRequest {
request.approvals.push({
approver: this.name,
approved: false,
comment: reason,
timestamp: new Date()
});
return request;
}
}
class Manager extends Approver {
constructor() {
super('Department Manager', 1000);
}
}
class Director extends Approver {
constructor() {
super('Director', 10000);
}
}
class VP extends Approver {
constructor() {
super('Vice President', 100000);
}
}
class CEO extends Approver {
constructor() {
super('CEO', Infinity);
}
}
// Build approval chain
const manager = new Manager();
manager
.setSuccessor(new Director())
.setSuccessor(new VP())
.setSuccessor(new CEO());
// Process requests
await manager.process({ id: '1', amount: 500, ... }); // Manager approves
await manager.process({ id: '2', amount: 5000, ... }); // Director approves
await manager.process({ id: '3', amount: 50000, ... }); // VP approves
await manager.process({ id: '4', amount: 500000, ... }); // CEO approvesWhen to Use
Use Chain of Responsibility when:
- Multiple objects may handle a request
- Handler should be determined at runtime
- You want to decouple senders and receivers
- Request should be passed along until handled
Don't use Chain of Responsibility when:
- There's always exactly one handler
- Handler selection is simple and static
- Performance is critical (chain traversal overhead)
Related Patterns
- Composite: Chain can follow composite structure
- Command: Commands can be passed through a chain
- Decorator: Similar structure, different intent (add behavior vs. handle request)
Command Pattern
Intent
Encapsulate a request as an object, thereby allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations.
The Problem
You need to:
- Decouple the object that invokes an operation from the object that performs it
- Queue, log, or schedule operations
- Support undo/redo functionality
- Build macro commands (composite operations)
Before: Tight Coupling
class TextEditor {
private text: string = '';
// Editor knows about ALL operations and their details
handleMenuClick(action: string) {
switch (action) {
case 'bold':
this.text = `<b>${this.getSelection()}</b>`;
break;
case 'italic':
this.text = `<i>${this.getSelection()}</i>`;
break;
case 'copy':
clipboard.write(this.getSelection());
break;
case 'paste':
this.text += clipboard.read();
break;
// Adding new operations means modifying this class
// No way to undo
// No way to queue or replay
}
}
}The Solution
Encapsulate each operation as a command object:
// Command interface
interface Command {
execute(): void;
undo(): void;
}
// Receiver - the object that knows how to perform the operation
class TextEditor {
private text: string = '';
private cursorPosition: number = 0;
getText(): string { return this.text; }
getSelection(): { start: number; end: number; text: string } {
// Return selected text range
return { start: 0, end: 5, text: this.text.substring(0, 5) };
}
insertAt(position: number, text: string): void {
this.text = this.text.slice(0, position) + text + this.text.slice(position);
}
deleteRange(start: number, end: number): string {
const deleted = this.text.substring(start, end);
this.text = this.text.slice(0, start) + this.text.slice(end);
return deleted;
}
setCursor(position: number): void {
this.cursorPosition = position;
}
}
// Concrete Commands
class InsertTextCommand implements Command {
private previousText: string = '';
constructor(
private editor: TextEditor,
private text: string,
private position: number
) {}
execute(): void {
this.previousText = this.editor.getText();
this.editor.insertAt(this.position, this.text);
}
undo(): void {
// Restore previous state
const currentText = this.editor.getText();
this.editor.deleteRange(0, currentText.length);
this.editor.insertAt(0, this.previousText);
}
}
class DeleteTextCommand implements Command {
private deletedText: string = '';
private deletedFrom: number = 0;
constructor(
private editor: TextEditor,
private start: number,
private end: number
) {}
execute(): void {
this.deletedFrom = this.start;
this.deletedText = this.editor.deleteRange(this.start, this.end);
}
undo(): void {
this.editor.insertAt(this.deletedFrom, this.deletedText);
}
}
class BoldCommand implements Command {
private selection: { start: number; end: number; text: string } | null = null;
constructor(private editor: TextEditor) {}
execute(): void {
this.selection = this.editor.getSelection();
const { start, end, text } = this.selection;
this.editor.deleteRange(start, end);
this.editor.insertAt(start, `<b>${text}</b>`);
}
undo(): void {
if (!this.selection) return;
const { start, text } = this.selection;
// Remove the bold tags
this.editor.deleteRange(start, start + text.length + 7); // <b></b> = 7 chars
this.editor.insertAt(start, text);
}
}
// Invoker - manages command execution and history
class CommandManager {
private history: Command[] = [];
private redoStack: Command[] = [];
execute(command: Command): void {
command.execute();
this.history.push(command);
this.redoStack = []; // Clear redo stack on new command
}
undo(): void {
const command = this.history.pop();
if (command) {
command.undo();
this.redoStack.push(command);
}
}
redo(): void {
const command = this.redoStack.pop();
if (command) {
command.execute();
this.history.push(command);
}
}
canUndo(): boolean {
return this.history.length > 0;
}
canRedo(): boolean {
return this.redoStack.length > 0;
}
}
// Usage
const editor = new TextEditor();
const commandManager = new CommandManager();
// Execute commands
commandManager.execute(new InsertTextCommand(editor, 'Hello World', 0));
console.log(editor.getText()); // "Hello World"
commandManager.execute(new BoldCommand(editor));
console.log(editor.getText()); // "<b>Hello</b> World" (assuming first 5 chars selected)
// Undo
commandManager.undo();
console.log(editor.getText()); // "Hello World"
commandManager.undo();
console.log(editor.getText()); // ""
// Redo
commandManager.redo();
console.log(editor.getText()); // "Hello World"Structure
┌───────────────┐ ┌─────────────────┐
│ Client │──────▶│ Invoker │
└───────────────┘ │ │
│ - commands[] │
│ + execute() │
│ + undo() │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Command │
│ <<interface>> │
│ + execute() │
│ + undo() │
└────────┬────────┘
△
┌────────────┴────────────┐
│ │
┌────────┴────────┐ ┌─────────┴───────┐
│ ConcreteCommand │ │ ConcreteCommand │
│ │ │ │
│ - receiver │ │ - receiver │
│ - state │ │ - state │
└────────┬────────┘ └────────┬────────┘
│ │
▼ ▼
┌───────────┐ ┌───────────┐
│ Receiver │ │ Receiver │
└───────────┘ └───────────┘JavaScript/TypeScript Implementations
Functional Commands
// Commands as simple objects with functions
interface FunctionalCommand {
execute: () => void;
undo: () => void;
description: string;
}
function createInsertCommand(
editor: TextEditor,
text: string,
position: number
): FunctionalCommand {
let previousText = '';
return {
description: `Insert "${text}" at position ${position}`,
execute() {
previousText = editor.getText();
editor.insertAt(position, text);
},
undo() {
editor.setText(previousText);
}
};
}
// Command factory
const CommandFactory = {
insert: (editor: TextEditor, text: string, pos: number) =>
createInsertCommand(editor, text, pos),
delete: (editor: TextEditor, start: number, end: number) => {
let deleted = '';
return {
description: `Delete from ${start} to ${end}`,
execute() {
deleted = editor.deleteRange(start, end);
},
undo() {
editor.insertAt(start, deleted);
}
};
},
replace: (editor: TextEditor, start: number, end: number, newText: string) => {
let oldText = '';
return {
description: `Replace text at ${start}-${end} with "${newText}"`,
execute() {
oldText = editor.getText().substring(start, end);
editor.deleteRange(start, end);
editor.insertAt(start, newText);
},
undo() {
editor.deleteRange(start, start + newText.length);
editor.insertAt(start, oldText);
}
};
}
};Async Commands
interface AsyncCommand<T = void> {
execute(): Promise<T>;
undo(): Promise<void>;
canUndo: boolean;
}
class CreateOrderCommand implements AsyncCommand<Order> {
private createdOrder: Order | null = null;
constructor(
private orderService: OrderService,
private orderData: CreateOrderDTO
) {}
get canUndo() {
return this.createdOrder !== null && this.createdOrder.status === 'pending';
}
async execute(): Promise<Order> {
this.createdOrder = await this.orderService.create(this.orderData);
return this.createdOrder;
}
async undo(): Promise<void> {
if (!this.canUndo) {
throw new Error('Cannot undo: order already processed');
}
await this.orderService.cancel(this.createdOrder!.id);
this.createdOrder = null;
}
}
class TransferFundsCommand implements AsyncCommand {
private transferId: string | null = null;
constructor(
private bankService: BankService,
private fromAccount: string,
private toAccount: string,
private amount: number
) {}
get canUndo() {
return this.transferId !== null;
}
async execute(): Promise<void> {
const result = await this.bankService.transfer(
this.fromAccount,
this.toAccount,
this.amount
);
this.transferId = result.transferId;
}
async undo(): Promise<void> {
if (!this.transferId) {
throw new Error('Nothing to undo');
}
// Reverse the transfer
await this.bankService.transfer(
this.toAccount,
this.fromAccount,
this.amount
);
this.transferId = null;
}
}
// Async command manager with transaction support
class AsyncCommandManager {
private history: AsyncCommand[] = [];
async execute<T>(command: AsyncCommand<T>): Promise<T> {
const result = await command.execute();
this.history.push(command);
return result;
}
async undo(): Promise<void> {
const command = this.history.pop();
if (command && command.canUndo) {
await command.undo();
}
}
// Execute multiple commands as a transaction
async executeTransaction(commands: AsyncCommand[]): Promise<void> {
const executed: AsyncCommand[] = [];
try {
for (const command of commands) {
await command.execute();
executed.push(command);
}
} catch (error) {
// Rollback executed commands in reverse order
for (const command of executed.reverse()) {
if (command.canUndo) {
await command.undo();
}
}
throw error;
}
this.history.push(...executed);
}
}Macro Commands (Composite)
class MacroCommand implements Command {
private commands: Command[] = [];
add(command: Command): this {
this.commands.push(command);
return this;
}
execute(): void {
for (const command of this.commands) {
command.execute();
}
}
undo(): void {
// Undo in reverse order
for (const command of [...this.commands].reverse()) {
command.undo();
}
}
}
// Usage - record a macro
const formatCodeMacro = new MacroCommand()
.add(new SelectAllCommand(editor))
.add(new IndentCommand(editor, 2))
.add(new TrimWhitespaceCommand(editor))
.add(new AddSemicolonsCommand(editor));
// Execute the macro
commandManager.execute(formatCodeMacro);
// Undo entire macro with single undo
commandManager.undo();Real-World Applications
1. Redux Actions as Commands
// Redux actions are essentially the Command pattern
// Action types
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const DELETE_TODO = 'DELETE_TODO';
// Action creators (command factories)
interface TodoAction {
type: string;
payload: any;
}
const addTodo = (text: string): TodoAction => ({
type: ADD_TODO,
payload: { id: Date.now(), text, completed: false }
});
const toggleTodo = (id: number): TodoAction => ({
type: TOGGLE_TODO,
payload: { id }
});
const deleteTodo = (id: number): TodoAction => ({
type: DELETE_TODO,
payload: { id }
});
// Reducer (command executor)
interface TodoState {
todos: Array<{ id: number; text: string; completed: boolean }>;
}
function todoReducer(state: TodoState, action: TodoAction): TodoState {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [...state.todos, action.payload]
};
case TOGGLE_TODO:
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo
)
};
case DELETE_TODO:
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload.id)
};
default:
return state;
}
}
// With undo support using redux-undo pattern
interface UndoableState<T> {
past: T[];
present: T;
future: T[];
}
function undoable<T>(reducer: (state: T, action: any) => T) {
const initialState: UndoableState<T> = {
past: [],
present: reducer(undefined as any, {}),
future: []
};
return function(state = initialState, action: any): UndoableState<T> {
const { past, present, future } = state;
switch (action.type) {
case 'UNDO':
if (past.length === 0) return state;
return {
past: past.slice(0, -1),
present: past[past.length - 1],
future: [present, ...future]
};
case 'REDO':
if (future.length === 0) return state;
return {
past: [...past, present],
present: future[0],
future: future.slice(1)
};
default:
const newPresent = reducer(present, action);
if (present === newPresent) return state;
return {
past: [...past, present],
present: newPresent,
future: []
};
}
};
}2. Task Queue / Job System
interface Job {
id: string;
execute(): Promise<void>;
onSuccess?: () => void;
onFailure?: (error: Error) => void;
retries: number;
maxRetries: number;
}
class JobQueue {
private queue: Job[] = [];
private processing = false;
private concurrency: number;
private activeJobs = 0;
constructor(concurrency: number = 1) {
this.concurrency = concurrency;
}
enqueue(job: Job): void {
this.queue.push(job);
this.processNext();
}
private async processNext(): Promise<void> {
if (this.processing || this.activeJobs >= this.concurrency) return;
if (this.queue.length === 0) return;
this.processing = true;
this.activeJobs++;
const job = this.queue.shift()!;
try {
await job.execute();
job.onSuccess?.();
} catch (error) {
if (job.retries < job.maxRetries) {
job.retries++;
this.queue.push(job); // Re-queue for retry
} else {
job.onFailure?.(error as Error);
}
} finally {
this.activeJobs--;
this.processing = false;
this.processNext();
}
}
}
// Job factory
function createEmailJob(to: string, subject: string, body: string): Job {
return {
id: `email-${Date.now()}`,
retries: 0,
maxRetries: 3,
async execute() {
await emailService.send({ to, subject, body });
},
onSuccess() {
console.log(`Email sent to ${to}`);
},
onFailure(error) {
console.error(`Failed to send email to ${to}:`, error);
}
};
}
function createImageProcessingJob(imageUrl: string, operations: string[]): Job {
return {
id: `image-${Date.now()}`,
retries: 0,
maxRetries: 2,
async execute() {
const image = await downloadImage(imageUrl);
for (const op of operations) {
await applyOperation(image, op);
}
await uploadProcessedImage(image);
}
};
}
// Usage
const jobQueue = new JobQueue(5); // 5 concurrent jobs
jobQueue.enqueue(createEmailJob('user@example.com', 'Welcome!', 'Hello...'));
jobQueue.enqueue(createImageProcessingJob('http://...', ['resize', 'compress']));3. Database Migration Commands
interface Migration {
version: number;
name: string;
up(): Promise<void>;
down(): Promise<void>;
}
class MigrationRunner {
private migrations: Migration[] = [];
private db: Database;
constructor(db: Database) {
this.db = db;
}
register(migration: Migration): void {
this.migrations.push(migration);
this.migrations.sort((a, b) => a.version - b.version);
}
async getCurrentVersion(): Promise<number> {
const result = await this.db.query(
'SELECT version FROM migrations ORDER BY version DESC LIMIT 1'
);
return result[0]?.version ?? 0;
}
async migrate(): Promise<void> {
const currentVersion = await this.getCurrentVersion();
for (const migration of this.migrations) {
if (migration.version > currentVersion) {
console.log(`Running migration: ${migration.name}`);
await this.db.beginTransaction();
try {
await migration.up();
await this.db.query(
'INSERT INTO migrations (version, name, applied_at) VALUES (?, ?, ?)',
[migration.version, migration.name, new Date()]
);
await this.db.commit();
} catch (error) {
await this.db.rollback();
throw error;
}
}
}
}
async rollback(steps: number = 1): Promise<void> {
const currentVersion = await this.getCurrentVersion();
const toRollback = this.migrations
.filter(m => m.version <= currentVersion)
.slice(-steps)
.reverse();
for (const migration of toRollback) {
console.log(`Rolling back: ${migration.name}`);
await this.db.beginTransaction();
try {
await migration.down();
await this.db.query('DELETE FROM migrations WHERE version = ?', [migration.version]);
await this.db.commit();
} catch (error) {
await this.db.rollback();
throw error;
}
}
}
}
// Define migrations as commands
const createUsersTable: Migration = {
version: 1,
name: 'create_users_table',
async up() {
await db.query(`
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
)
`);
},
async down() {
await db.query('DROP TABLE users');
}
};
const addUserNameColumn: Migration = {
version: 2,
name: 'add_user_name_column',
async up() {
await db.query('ALTER TABLE users ADD COLUMN name VARCHAR(255)');
},
async down() {
await db.query('ALTER TABLE users DROP COLUMN name');
}
};
// Usage
const runner = new MigrationRunner(db);
runner.register(createUsersTable);
runner.register(addUserNameColumn);
await runner.migrate(); // Run all pending migrations
await runner.rollback(1); // Undo last migrationWhen to Use
Use Command when:
- You need to parameterize objects with operations
- You need to queue, log, or schedule operations
- You need undo/redo functionality
- You want to structure around high-level operations built on primitive operations
Don't use Command when:
- Operations are simple and don't need to be undone
- You don't need to queue or log operations
- The overhead isn't justified
Related Patterns
- Memento: Can store state for undo instead of reverse operation
- Composite: MacroCommand is a Composite of Commands
- Strategy: Both encapsulate behavior, but Command focuses on requests with undo
- Chain of Responsibility: Can pass commands through a chain
Composite Pattern
Intent
Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.
The Problem
You need to represent hierarchical structures where:
- Containers can hold both individual items and other containers
- Clients should treat simple and complex elements the same way
- Operations should propagate through the hierarchy
Common Scenarios
- File systems (files and folders)
- UI components (elements and containers)
- Organization structures (employees and departments)
- Menu systems (items and submenus)
- Graphics (shapes and groups of shapes)
The Solution
Define a component interface that both leaf nodes and composite nodes implement:
// Component interface
interface FileSystemNode {
getName(): string;
getSize(): number;
print(indent?: string): void;
getPath(): string;
find(predicate: (node: FileSystemNode) => boolean): FileSystemNode[];
}
// Leaf - no children
class File implements FileSystemNode {
constructor(
private name: string,
private size: number,
private parent?: Directory
) {}
getName(): string {
return this.name;
}
getSize(): number {
return this.size;
}
getPath(): string {
return this.parent
? `${this.parent.getPath()}/${this.name}`
: this.name;
}
print(indent: string = ''): void {
console.log(`${indent}📄 ${this.name} (${this.formatSize(this.size)})`);
}
find(predicate: (node: FileSystemNode) => boolean): FileSystemNode[] {
return predicate(this) ? [this] : [];
}
private formatSize(bytes: number): string {
if (bytes < 1024) return `${bytes}B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
}
}
// Composite - can have children
class Directory implements FileSystemNode {
private children: FileSystemNode[] = [];
constructor(
private name: string,
private parent?: Directory
) {}
getName(): string {
return this.name;
}
getSize(): number {
// Aggregate size from all children
return this.children.reduce((total, child) => total + child.getSize(), 0);
}
getPath(): string {
return this.parent
? `${this.parent.getPath()}/${this.name}`
: this.name;
}
print(indent: string = ''): void {
console.log(`${indent}📁 ${this.name}/`);
this.children.forEach(child => child.print(indent + ' '));
}
find(predicate: (node: FileSystemNode) => boolean): FileSystemNode[] {
const results: FileSystemNode[] = predicate(this) ? [this] : [];
this.children.forEach(child => {
results.push(...child.find(predicate));
});
return results;
}
// Composite-specific methods
add(node: FileSystemNode): this {
this.children.push(node);
return this;
}
remove(node: FileSystemNode): void {
const index = this.children.indexOf(node);
if (index !== -1) {
this.children.splice(index, 1);
}
}
getChildren(): FileSystemNode[] {
return [...this.children];
}
}
// Usage
const root = new Directory('project');
const src = new Directory('src', root);
src.add(new File('index.ts', 1024, src));
src.add(new File('app.ts', 2048, src));
const components = new Directory('components', src);
components.add(new File('Button.tsx', 512, components));
components.add(new File('Modal.tsx', 1536, components));
src.add(components);
root.add(src);
root.add(new File('package.json', 256, root));
root.add(new File('README.md', 4096, root));
// Uniform treatment
root.print();
// 📁 project/
// 📁 src/
// 📄 index.ts (1.0KB)
// 📄 app.ts (2.0KB)
// 📁 components/
// 📄 Button.tsx (512B)
// 📄 Modal.tsx (1.5KB)
// 📄 package.json (256B)
// 📄 README.md (4.0KB)
console.log(`Total size: ${root.getSize()} bytes`); // 9472 bytes
// Find all TypeScript files
const tsFiles = root.find(node => node.getName().endsWith('.ts'));
console.log('TypeScript files:', tsFiles.map(f => f.getPath()));Structure
┌─────────────────────┐
│ Component │
│ <<interface>> │
├─────────────────────┤
┌──────▶│ + operation() │◀───────┐
│ │ + add(Component) │ │
│ │ + remove(Component) │ │
│ │ + getChild(i) │ │
│ └─────────────────────┘ │
│ △ │
│ │ │
│ ┌─────────┴─────────┐ │
│ │ │ │
│ ┌────┴────┐ ┌─────┴─────┐ │
│ │ Leaf │ │ Composite │────┘
│ │ │ │ │ children
│ │+operation│ │+operation │
│ └─────────┘ │+add │
│ │+remove │
│ │+getChild │
│ └───────────┘
│ │
└───────────────────────────┘
uses uniformlyReal-World Applications
1. UI Component Tree
interface UIComponent {
render(): string;
getBoundingBox(): { width: number; height: number };
handleClick(x: number, y: number): void;
setStyle(style: Partial<CSSProperties>): void;
}
abstract class BaseComponent implements UIComponent {
protected style: CSSProperties = {};
protected eventHandlers: Map<string, Function[]> = new Map();
abstract render(): string;
abstract getBoundingBox(): { width: number; height: number };
handleClick(x: number, y: number): void {
const handlers = this.eventHandlers.get('click') || [];
handlers.forEach(h => h({ x, y }));
}
setStyle(style: Partial<CSSProperties>): void {
this.style = { ...this.style, ...style };
}
on(event: string, handler: Function): void {
if (!this.eventHandlers.has(event)) {
this.eventHandlers.set(event, []);
}
this.eventHandlers.get(event)!.push(handler);
}
protected styleToString(): string {
return Object.entries(this.style)
.map(([key, value]) => `${this.camelToKebab(key)}: ${value}`)
.join('; ');
}
private camelToKebab(str: string): string {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
}
// Leaf components
class Button extends BaseComponent {
constructor(private label: string) {
super();
}
render(): string {
return `<button style="${this.styleToString()}">${this.label}</button>`;
}
getBoundingBox() {
return { width: 100, height: 40 };
}
}
class TextInput extends BaseComponent {
constructor(private placeholder: string) {
super();
}
render(): string {
return `<input type="text" placeholder="${this.placeholder}" style="${this.styleToString()}" />`;
}
getBoundingBox() {
return { width: 200, height: 36 };
}
}
class Image extends BaseComponent {
constructor(private src: string, private alt: string) {
super();
}
render(): string {
return `<img src="${this.src}" alt="${this.alt}" style="${this.styleToString()}" />`;
}
getBoundingBox() {
const width = parseInt(this.style.width as string) || 100;
const height = parseInt(this.style.height as string) || 100;
return { width, height };
}
}
// Composite components
class Container extends BaseComponent {
protected children: UIComponent[] = [];
add(child: UIComponent): this {
this.children.push(child);
return this;
}
remove(child: UIComponent): void {
const index = this.children.indexOf(child);
if (index !== -1) this.children.splice(index, 1);
}
render(): string {
const childrenHtml = this.children.map(c => c.render()).join('\n');
return `<div style="${this.styleToString()}">\n${childrenHtml}\n</div>`;
}
getBoundingBox() {
let width = 0;
let height = 0;
this.children.forEach(child => {
const box = child.getBoundingBox();
width = Math.max(width, box.width);
height += box.height;
});
return { width, height };
}
handleClick(x: number, y: number): void {
super.handleClick(x, y);
// Propagate to children
this.children.forEach(child => child.handleClick(x, y));
}
setStyle(style: Partial<CSSProperties>): void {
super.setStyle(style);
// Optionally propagate certain styles to children
}
}
class Form extends Container {
constructor(private action: string, private method: string = 'POST') {
super();
}
render(): string {
const childrenHtml = this.children.map(c => c.render()).join('\n');
return `<form action="${this.action}" method="${this.method}" style="${this.styleToString()}">\n${childrenHtml}\n</form>`;
}
}
class FlexContainer extends Container {
constructor(direction: 'row' | 'column' = 'row') {
super();
this.style = {
display: 'flex',
flexDirection: direction
};
}
getBoundingBox() {
const isRow = this.style.flexDirection === 'row';
let width = 0;
let height = 0;
this.children.forEach(child => {
const box = child.getBoundingBox();
if (isRow) {
width += box.width;
height = Math.max(height, box.height);
} else {
width = Math.max(width, box.width);
height += box.height;
}
});
return { width, height };
}
}
// Usage - build UI tree
const loginForm = new Form('/api/login')
.add(new TextInput('Email'))
.add(new TextInput('Password'))
.add(new Button('Login'));
const sidebar = new FlexContainer('column');
sidebar.add(new Image('/logo.png', 'Logo'));
sidebar.add(loginForm);
sidebar.setStyle({ width: '300px', padding: '20px' });
const mainContent = new Container();
mainContent.add(new Button('Action 1'));
mainContent.add(new Button('Action 2'));
const layout = new FlexContainer('row');
layout.add(sidebar);
layout.add(mainContent);
console.log(layout.render());
console.log('Total size:', layout.getBoundingBox());2. Menu System
interface MenuItem {
getName(): string;
getPrice(): number | null; // null for categories
isAvailable(): boolean;
print(indent?: string): void;
accept(visitor: MenuVisitor): void;
}
interface MenuVisitor {
visitItem(item: MenuItemLeaf): void;
visitCategory(category: MenuCategory): void;
}
class MenuItemLeaf implements MenuItem {
constructor(
private name: string,
private price: number,
private available: boolean = true,
private description?: string,
private allergens?: string[]
) {}
getName(): string {
return this.name;
}
getPrice(): number {
return this.price;
}
isAvailable(): boolean {
return this.available;
}
setAvailable(available: boolean): void {
this.available = available;
}
print(indent: string = ''): void {
const status = this.available ? '' : ' [SOLD OUT]';
const price = `$${this.price.toFixed(2)}`;
console.log(`${indent}• ${this.name} - ${price}${status}`);
if (this.description) {
console.log(`${indent} ${this.description}`);
}
if (this.allergens?.length) {
console.log(`${indent} ⚠️ Contains: ${this.allergens.join(', ')}`);
}
}
accept(visitor: MenuVisitor): void {
visitor.visitItem(this);
}
}
class MenuCategory implements MenuItem {
private items: MenuItem[] = [];
constructor(
private name: string,
private description?: string
) {}
getName(): string {
return this.name;
}
getPrice(): number | null {
return null; // Categories don't have prices
}
isAvailable(): boolean {
// Category is available if any item is available
return this.items.some(item => item.isAvailable());
}
add(item: MenuItem): this {
this.items.push(item);
return this;
}
remove(item: MenuItem): void {
const index = this.items.indexOf(item);
if (index !== -1) this.items.splice(index, 1);
}
getItems(): MenuItem[] {
return [...this.items];
}
print(indent: string = ''): void {
console.log(`${indent}【${this.name}】`);
if (this.description) {
console.log(`${indent} ${this.description}`);
}
this.items.forEach(item => item.print(indent + ' '));
}
accept(visitor: MenuVisitor): void {
visitor.visitCategory(this);
this.items.forEach(item => item.accept(visitor));
}
}
// Visitors for different operations
class TotalPriceCalculator implements MenuVisitor {
total: number = 0;
visitItem(item: MenuItemLeaf): void {
if (item.isAvailable()) {
this.total += item.getPrice();
}
}
visitCategory(_category: MenuCategory): void {
// Categories don't contribute to total
}
}
class AvailableItemsCollector implements MenuVisitor {
items: string[] = [];
visitItem(item: MenuItemLeaf): void {
if (item.isAvailable()) {
this.items.push(item.getName());
}
}
visitCategory(_category: MenuCategory): void {}
}
// Usage
const menu = new MenuCategory('Restaurant Menu');
const appetizers = new MenuCategory('Appetizers', 'Start your meal right');
appetizers
.add(new MenuItemLeaf('Soup of the Day', 6.99, true, 'Ask your server'))
.add(new MenuItemLeaf('Nachos', 9.99, true, 'Loaded with cheese and jalapeños', ['dairy']))
.add(new MenuItemLeaf('Wings', 11.99, false, '12 crispy wings'));
const mains = new MenuCategory('Main Courses');
mains
.add(new MenuItemLeaf('Burger', 14.99, true, 'Angus beef with all the fixings', ['gluten', 'dairy']))
.add(new MenuItemLeaf('Salmon', 22.99, true, 'Atlantic salmon with vegetables', ['fish']))
.add(new MenuItemLeaf('Pasta', 16.99, true, 'Fresh pasta with marinara', ['gluten']));
const desserts = new MenuCategory('Desserts');
desserts
.add(new MenuItemLeaf('Ice Cream', 5.99, true, 'Three scoops', ['dairy']))
.add(new MenuItemLeaf('Cake', 7.99, true, 'Chocolate layer cake', ['gluten', 'dairy', 'eggs']));
menu.add(appetizers);
menu.add(mains);
menu.add(desserts);
// Print entire menu
menu.print();
// Use visitors
const priceCalc = new TotalPriceCalculator();
menu.accept(priceCalc);
console.log(`\nTotal menu value: $${priceCalc.total.toFixed(2)}`);
const availableCollector = new AvailableItemsCollector();
menu.accept(availableCollector);
console.log('\nAvailable items:', availableCollector.items);3. Organization Structure
interface OrganizationUnit {
getName(): string;
getSalaryBudget(): number;
getHeadcount(): number;
print(indent?: string): void;
findByName(name: string): OrganizationUnit | null;
}
class Employee implements OrganizationUnit {
constructor(
private name: string,
private title: string,
private salary: number
) {}
getName(): string {
return this.name;
}
getTitle(): string {
return this.title;
}
getSalaryBudget(): number {
return this.salary;
}
getHeadcount(): number {
return 1;
}
print(indent: string = ''): void {
console.log(`${indent}👤 ${this.name} (${this.title}) - $${this.salary.toLocaleString()}`);
}
findByName(name: string): OrganizationUnit | null {
return this.name.toLowerCase().includes(name.toLowerCase()) ? this : null;
}
}
class Department implements OrganizationUnit {
private members: OrganizationUnit[] = [];
constructor(
private name: string,
private leader?: Employee
) {}
getName(): string {
return this.name;
}
getLeader(): Employee | undefined {
return this.leader;
}
setLeader(leader: Employee): void {
this.leader = leader;
// Ensure leader is in members
if (!this.members.includes(leader)) {
this.members.unshift(leader);
}
}
add(member: OrganizationUnit): this {
this.members.push(member);
return this;
}
remove(member: OrganizationUnit): void {
const index = this.members.indexOf(member);
if (index !== -1) this.members.splice(index, 1);
}
getSalaryBudget(): number {
return this.members.reduce((total, member) => total + member.getSalaryBudget(), 0);
}
getHeadcount(): number {
return this.members.reduce((total, member) => total + member.getHeadcount(), 0);
}
print(indent: string = ''): void {
const budget = this.getSalaryBudget().toLocaleString();
console.log(`${indent}🏢 ${this.name} (${this.getHeadcount()} people, $${budget} budget)`);
this.members.forEach(member => member.print(indent + ' '));
}
findByName(name: string): OrganizationUnit | null {
if (this.name.toLowerCase().includes(name.toLowerCase())) {
return this;
}
for (const member of this.members) {
const found = member.findByName(name);
if (found) return found;
}
return null;
}
getDirectReports(): OrganizationUnit[] {
return [...this.members];
}
}
// Build organization
const ceo = new Employee('Alice Smith', 'CEO', 500000);
const engineering = new Department('Engineering');
const vpEng = new Employee('Bob Johnson', 'VP Engineering', 300000);
engineering.setLeader(vpEng);
const frontend = new Department('Frontend Team');
frontend.setLeader(new Employee('Carol Williams', 'Frontend Lead', 180000));
frontend.add(new Employee('Dave Brown', 'Senior Developer', 150000));
frontend.add(new Employee('Eve Davis', 'Developer', 120000));
frontend.add(new Employee('Frank Miller', 'Developer', 110000));
const backend = new Department('Backend Team');
backend.setLeader(new Employee('Grace Wilson', 'Backend Lead', 180000));
backend.add(new Employee('Henry Taylor', 'Senior Developer', 150000));
backend.add(new Employee('Ivy Anderson', 'Developer', 120000));
engineering.add(frontend);
engineering.add(backend);
const sales = new Department('Sales');
sales.setLeader(new Employee('Jack Thomas', 'VP Sales', 280000));
sales.add(new Employee('Karen White', 'Account Executive', 90000));
sales.add(new Employee('Leo Harris', 'Account Executive', 85000));
const company = new Department('Acme Corp');
company.setLeader(ceo);
company.add(engineering);
company.add(sales);
// Operations work uniformly
company.print();
console.log(`\nTotal company budget: $${company.getSalaryBudget().toLocaleString()}`);
console.log(`Total headcount: ${company.getHeadcount()}`);
// Find specific unit
const found = company.findByName('Frontend');
if (found) {
console.log('\nFound:');
found.print();
}When to Use
Use Composite when:
- You want to represent part-whole hierarchies
- You want clients to treat leaf and composite objects uniformly
- The structure can be represented as a tree
- Operations should propagate through the hierarchy
Don't use Composite when:
- The structure isn't hierarchical
- Leaf and composite behaviors differ significantly
- You need to restrict which components can be nested
Common Variations
Transparent vs Safe Composite
Transparent: Component interface includes add/remove (shown above)
- Pro: Maximum uniformity
- Con: Leaf nodes have meaningless add/remove methods
Safe: Only Composite has add/remove
- Pro: Leaves don't have inappropriate methods
- Con: Client must know if dealing with leaf or composite
// Safe approach - use type guards
function isComposite(node: FileSystemNode): node is Directory {
return 'add' in node;
}
if (isComposite(node)) {
node.add(newFile);
}Related Patterns
- Decorator: Also recursive composition, but adds behavior vs structure
- Iterator: Often used to traverse composite structures
- Visitor: Can perform operations across composite structure
- Flyweight: Can share leaf nodes to save memory
Decorator Pattern
Intent
Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
The Problem
You need to add behavior to objects, but:
- Inheritance is static and applies to entire class
- You want to add/remove behavior at runtime
- Combining behaviors leads to class explosion
The Class Explosion Problem
// Base class
class Coffee {
cost(): number { return 2; }
description(): string { return 'Coffee'; }
}
// Now we need variations...
class CoffeeWithMilk extends Coffee {
cost() { return super.cost() + 0.5; }
description() { return super.description() + ', Milk'; }
}
class CoffeeWithSugar extends Coffee {
cost() { return super.cost() + 0.25; }
description() { return super.description() + ', Sugar'; }
}
// Combinations start exploding
class CoffeeWithMilkAndSugar extends Coffee { /* ... */ }
class CoffeeWithMilkAndWhip extends Coffee { /* ... */ }
class CoffeeWithMilkAndSugarAndWhip extends Coffee { /* ... */ }
class CoffeeWithDoubleMilk extends Coffee { /* ... */ }
// 🔥 This doesn't scale!The Solution
Wrap objects with decorator objects that add behavior:
// Component interface
interface Coffee {
cost(): number;
description(): string;
}
// Concrete component
class SimpleCoffee implements Coffee {
cost() { return 2; }
description() { return 'Coffee'; }
}
class Espresso implements Coffee {
cost() { return 2.5; }
description() { return 'Espresso'; }
}
// Base decorator
abstract class CoffeeDecorator implements Coffee {
constructor(protected coffee: Coffee) {}
cost(): number {
return this.coffee.cost();
}
description(): string {
return this.coffee.description();
}
}
// Concrete decorators
class MilkDecorator extends CoffeeDecorator {
cost() { return this.coffee.cost() + 0.5; }
description() { return `${this.coffee.description()}, Milk`; }
}
class SugarDecorator extends CoffeeDecorator {
cost() { return this.coffee.cost() + 0.25; }
description() { return `${this.coffee.description()}, Sugar`; }
}
class WhipDecorator extends CoffeeDecorator {
cost() { return this.coffee.cost() + 0.75; }
description() { return `${this.coffee.description()}, Whipped Cream`; }
}
class VanillaDecorator extends CoffeeDecorator {
cost() { return this.coffee.cost() + 0.4; }
description() { return `${this.coffee.description()}, Vanilla`; }
}
// Usage - compose any combination!
let coffee: Coffee = new Espresso();
coffee = new MilkDecorator(coffee);
coffee = new MilkDecorator(coffee); // Double milk!
coffee = new VanillaDecorator(coffee);
coffee = new WhipDecorator(coffee);
console.log(coffee.description()); // Espresso, Milk, Milk, Vanilla, Whipped Cream
console.log(coffee.cost()); // 2.5 + 0.5 + 0.5 + 0.4 + 0.75 = 4.65Structure
┌─────────────────────┐
│ Component │◄─────────────────────────────┐
│ <<interface>> │ │
├─────────────────────┤ │
│ + operation() │ │
└─────────────────────┘ │
△ │
│ │
┌─────┴─────┐ │
│ │ │
┌───┴───┐ ┌────┴────────────┐ │
│Concrete│ │ Decorator │─────────────────────┘
│Component│ │ │ wraps component
└─────────┘ │ - component │
│ + operation() │
└─────────────────┘
△
│
┌─────────┴─────────┐
│ │
┌──────┴──────┐ ┌───────┴─────┐
│ DecoratorA │ │ DecoratorB │
│+ operation()│ │+ operation()│
└─────────────┘ └─────────────┘JavaScript/TypeScript Implementations
Function Decorators (Most Idiomatic JS)
// Higher-order function that adds behavior
type AsyncFunction<T> = (...args: any[]) => Promise<T>;
// Retry decorator
function withRetry<T>(
fn: AsyncFunction<T>,
maxRetries: number = 3,
delay: number = 1000
): AsyncFunction<T> {
return async (...args: any[]): Promise<T> => {
let lastError: Error;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn(...args);
} catch (error) {
lastError = error as Error;
if (attempt < maxRetries - 1) {
await new Promise(r => setTimeout(r, delay * Math.pow(2, attempt)));
}
}
}
throw lastError!;
};
}
// Logging decorator
function withLogging<T>(
fn: AsyncFunction<T>,
name: string
): AsyncFunction<T> {
return async (...args: any[]): Promise<T> => {
console.log(`[${name}] Called with:`, args);
const start = Date.now();
try {
const result = await fn(...args);
console.log(`[${name}] Completed in ${Date.now() - start}ms`);
return result;
} catch (error) {
console.error(`[${name}] Failed after ${Date.now() - start}ms:`, error);
throw error;
}
};
}
// Cache decorator
function withCache<T>(
fn: AsyncFunction<T>,
keyFn: (...args: any[]) => string,
ttlMs: number = 60000
): AsyncFunction<T> {
const cache = new Map<string, { value: T; expires: number }>();
return async (...args: any[]): Promise<T> => {
const key = keyFn(...args);
const cached = cache.get(key);
if (cached && cached.expires > Date.now()) {
return cached.value;
}
const result = await fn(...args);
cache.set(key, { value: result, expires: Date.now() + ttlMs });
return result;
};
}
// Timeout decorator
function withTimeout<T>(
fn: AsyncFunction<T>,
timeoutMs: number
): AsyncFunction<T> {
return async (...args: any[]): Promise<T> => {
return Promise.race([
fn(...args),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeoutMs)
)
]);
};
}
// Compose decorators
async function fetchUserById(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// Stack decorators - order matters!
const fetchUser = withLogging(
withRetry(
withTimeout(
withCache(
fetchUserById,
(id) => `user:${id}`,
5 * 60 * 1000 // 5 min cache
),
5000 // 5 sec timeout
),
3 // 3 retries
),
'fetchUser'
);
// Usage
const user = await fetchUser('123');TypeScript Decorators (ES Decorators)
// Method decorators using TypeScript's decorator syntax
// Logging decorator
function Log(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
console.log(`[${propertyKey}] Called with:`, args);
const start = Date.now();
try {
const result = await originalMethod.apply(this, args);
console.log(`[${propertyKey}] Returned:`, result);
return result;
} catch (error) {
console.error(`[${propertyKey}] Threw:`, error);
throw error;
} finally {
console.log(`[${propertyKey}] Duration: ${Date.now() - start}ms`);
}
};
return descriptor;
}
// Memoization decorator
function Memoize(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
const cache = new Map<string, any>();
descriptor.value = function (...args: any[]) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = originalMethod.apply(this, args);
cache.set(key, result);
return result;
};
return descriptor;
}
// Debounce decorator
function Debounce(ms: number) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
let timeoutId: NodeJS.Timeout;
descriptor.value = function (...args: any[]) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
originalMethod.apply(this, args);
}, ms);
};
return descriptor;
};
}
// Validate decorator
function Validate(schema: Schema) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const validation = schema.validate(args[0]);
if (validation.error) {
throw new ValidationError(validation.error.message);
}
return originalMethod.apply(this, args);
};
return descriptor;
};
}
// Usage with class
class UserService {
@Log
@Validate(userSchema)
async createUser(data: CreateUserDTO): Promise<User> {
// Implementation
}
@Log
@Memoize
async getUserById(id: string): Promise<User> {
// Implementation
}
@Debounce(300)
onSearchInput(query: string): void {
// Implementation
}
}Middleware Pattern (Decorator for HTTP)
// Express-style middleware is essentially the decorator pattern
type Request = { body: any; headers: Record<string, string>; user?: User };
type Response = { json: (data: any) => void; status: (code: number) => Response };
type NextFunction = () => Promise<void>;
type Middleware = (req: Request, res: Response, next: NextFunction) => Promise<void>;
type Handler = (req: Request, res: Response) => Promise<void>;
// Decorators as middleware
const authenticate: Middleware = async (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
req.user = await verifyToken(token);
await next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
};
const rateLimit = (maxRequests: number, windowMs: number): Middleware => {
const requests = new Map<string, number[]>();
return async (req, res, next) => {
const ip = req.headers['x-forwarded-for'] || 'unknown';
const now = Date.now();
const windowStart = now - windowMs;
const userRequests = (requests.get(ip) || [])
.filter(time => time > windowStart);
if (userRequests.length >= maxRequests) {
return res.status(429).json({ error: 'Too many requests' });
}
userRequests.push(now);
requests.set(ip, userRequests);
await next();
};
};
const validateBody = (schema: Schema): Middleware => {
return async (req, res, next) => {
const result = schema.validate(req.body);
if (result.error) {
return res.status(400).json({ error: result.error.message });
}
await next();
};
};
const logRequest: Middleware = async (req, res, next) => {
const start = Date.now();
console.log(`→ ${req.method} ${req.path}`);
await next();
console.log(`← ${req.method} ${req.path} ${Date.now() - start}ms`);
};
// Compose middleware
function compose(...middlewares: Middleware[]): (handler: Handler) => Handler {
return (handler: Handler): Handler => {
return async (req: Request, res: Response) => {
let index = 0;
const next = async (): Promise<void> => {
if (index < middlewares.length) {
const middleware = middlewares[index++];
await middleware(req, res, next);
} else {
await handler(req, res);
}
};
await next();
};
};
}
// Usage
const createUserHandler: Handler = async (req, res) => {
const user = await userService.create(req.body);
res.json(user);
};
// Decorated handler
const decoratedHandler = compose(
logRequest,
rateLimit(100, 60000),
authenticate,
validateBody(createUserSchema)
)(createUserHandler);
// Register route
app.post('/users', decoratedHandler);Real-World Applications
1. Stream Decorators
interface DataStream {
read(): Promise<Buffer | null>;
write(data: Buffer): Promise<void>;
close(): Promise<void>;
}
class FileStream implements DataStream {
private handle: fs.promises.FileHandle | null = null;
constructor(private path: string, private mode: 'r' | 'w') {}
async read(): Promise<Buffer | null> {
if (!this.handle) {
this.handle = await fs.promises.open(this.path, this.mode);
}
const buffer = Buffer.alloc(4096);
const { bytesRead } = await this.handle.read(buffer, 0, 4096, null);
return bytesRead > 0 ? buffer.slice(0, bytesRead) : null;
}
async write(data: Buffer): Promise<void> {
if (!this.handle) {
this.handle = await fs.promises.open(this.path, this.mode);
}
await this.handle.write(data);
}
async close(): Promise<void> {
await this.handle?.close();
this.handle = null;
}
}
// Base decorator
abstract class StreamDecorator implements DataStream {
constructor(protected stream: DataStream) {}
async read(): Promise<Buffer | null> {
return this.stream.read();
}
async write(data: Buffer): Promise<void> {
return this.stream.write(data);
}
async close(): Promise<void> {
return this.stream.close();
}
}
// Compression decorator
class GzipStream extends StreamDecorator {
async read(): Promise<Buffer | null> {
const data = await this.stream.read();
if (!data) return null;
return new Promise((resolve, reject) => {
zlib.gunzip(data, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
}
async write(data: Buffer): Promise<void> {
const compressed = await new Promise<Buffer>((resolve, reject) => {
zlib.gzip(data, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
return this.stream.write(compressed);
}
}
// Encryption decorator
class EncryptedStream extends StreamDecorator {
constructor(
stream: DataStream,
private key: Buffer,
private iv: Buffer
) {
super(stream);
}
async read(): Promise<Buffer | null> {
const data = await this.stream.read();
if (!data) return null;
const decipher = crypto.createDecipheriv('aes-256-cbc', this.key, this.iv);
return Buffer.concat([decipher.update(data), decipher.final()]);
}
async write(data: Buffer): Promise<void> {
const cipher = crypto.createCipheriv('aes-256-cbc', this.key, this.iv);
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
return this.stream.write(encrypted);
}
}
// Buffered decorator
class BufferedStream extends StreamDecorator {
private buffer: Buffer[] = [];
private bufferSize: number;
constructor(stream: DataStream, bufferSize: number = 8192) {
super(stream);
this.bufferSize = bufferSize;
}
async write(data: Buffer): Promise<void> {
this.buffer.push(data);
const totalSize = this.buffer.reduce((sum, b) => sum + b.length, 0);
if (totalSize >= this.bufferSize) {
await this.flush();
}
}
async flush(): Promise<void> {
if (this.buffer.length > 0) {
await this.stream.write(Buffer.concat(this.buffer));
this.buffer = [];
}
}
async close(): Promise<void> {
await this.flush();
await this.stream.close();
}
}
// Usage - compose stream behaviors
const key = crypto.randomBytes(32);
const iv = crypto.randomBytes(16);
// Writing: buffer → compress → encrypt → file
let writeStream: DataStream = new FileStream('data.enc', 'w');
writeStream = new EncryptedStream(writeStream, key, iv);
writeStream = new GzipStream(writeStream);
writeStream = new BufferedStream(writeStream);
await writeStream.write(Buffer.from('Hello, World!'));
await writeStream.close();
// Reading: file → decrypt → decompress
let readStream: DataStream = new FileStream('data.enc', 'r');
readStream = new EncryptedStream(readStream, key, iv);
readStream = new GzipStream(readStream);
const data = await readStream.read();
console.log(data?.toString()); // Hello, World!2. React Higher-Order Components
// HOC is the decorator pattern for React components
interface WithLoadingProps {
isLoading: boolean;
}
// Loading decorator
function withLoading<P extends object>(
WrappedComponent: React.ComponentType<P>
): React.FC<P & WithLoadingProps> {
return function WithLoadingComponent({ isLoading, ...props }: P & WithLoadingProps) {
if (isLoading) {
return <div className="spinner">Loading...</div>;
}
return <WrappedComponent {...(props as P)} />;
};
}
// Error boundary decorator
function withErrorBoundary<P extends object>(
WrappedComponent: React.ComponentType<P>,
FallbackComponent: React.ComponentType<{ error: Error }>
): React.FC<P> {
return class ErrorBoundary extends React.Component<P, { error: Error | null }> {
state = { error: null };
static getDerivedStateFromError(error: Error) {
return { error };
}
render() {
if (this.state.error) {
return <FallbackComponent error={this.state.error} />;
}
return <WrappedComponent {...this.props} />;
}
};
}
// Auth decorator
function withAuth<P extends object>(
WrappedComponent: React.ComponentType<P>,
requiredRole?: string
): React.FC<P> {
return function WithAuthComponent(props: P) {
const { user, isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
if (requiredRole && user?.role !== requiredRole) {
return <div>Access Denied</div>;
}
return <WrappedComponent {...props} />;
};
}
// Analytics decorator
function withAnalytics<P extends object>(
WrappedComponent: React.ComponentType<P>,
eventName: string
): React.FC<P> {
return function WithAnalyticsComponent(props: P) {
useEffect(() => {
analytics.track(`${eventName}_viewed`);
return () => analytics.track(`${eventName}_left`);
}, []);
return <WrappedComponent {...props} />;
};
}
// Usage - compose HOCs
const UserDashboard: React.FC<DashboardProps> = ({ data }) => {
return <div>{/* Dashboard content */}</div>;
};
// Apply decorators
export default withAnalytics(
withErrorBoundary(
withAuth(
withLoading(UserDashboard),
'admin'
),
ErrorFallback
),
'admin_dashboard'
);3. API Response Decorators
interface APIResponse<T> {
data: T;
meta?: Record<string, unknown>;
}
// Base transformer
type ResponseTransformer<T, U> = (response: APIResponse<T>) => APIResponse<U>;
// Decorator: Add pagination metadata
function withPagination<T>(
page: number,
pageSize: number,
total: number
): ResponseTransformer<T[], T[]> {
return (response) => ({
...response,
meta: {
...response.meta,
pagination: {
page,
pageSize,
total,
totalPages: Math.ceil(total / pageSize),
hasNext: page * pageSize < total,
hasPrev: page > 1
}
}
});
}
// Decorator: Add timing info
function withTiming<T>(startTime: number): ResponseTransformer<T, T> {
return (response) => ({
...response,
meta: {
...response.meta,
timing: {
duration: Date.now() - startTime,
timestamp: new Date().toISOString()
}
}
});
}
// Decorator: Transform data shape
function withFieldSelection<T extends Record<string, unknown>>(
fields: (keyof T)[]
): ResponseTransformer<T[], Partial<T>[]> {
return (response) => ({
...response,
data: response.data.map(item =>
fields.reduce((acc, field) => {
acc[field] = item[field];
return acc;
}, {} as Partial<T>)
)
});
}
// Decorator: Add HATEOAS links
function withLinks<T>(
linkGenerator: (data: T) => Record<string, string>
): ResponseTransformer<T, T & { _links: Record<string, string> }> {
return (response) => ({
...response,
data: {
...response.data,
_links: linkGenerator(response.data)
}
});
}
// Compose transformers
function compose<T>(...transformers: ResponseTransformer<any, any>[]) {
return (initial: APIResponse<T>): APIResponse<any> => {
return transformers.reduce(
(response, transformer) => transformer(response),
initial
);
};
}
// Usage in API handler
async function getUsers(req: Request, res: Response) {
const startTime = Date.now();
const { page = 1, pageSize = 20, fields } = req.query;
const [users, total] = await Promise.all([
userRepo.findPaginated(page, pageSize),
userRepo.count()
]);
let response: APIResponse<User[]> = { data: users };
// Apply decorators
const transform = compose<User[]>(
withPagination(page, pageSize, total),
withTiming(startTime),
...(fields ? [withFieldSelection(fields.split(','))] : [])
);
res.json(transform(response));
}When to Use
Use Decorator when:
- You need to add responsibilities to objects without affecting others
- Extension by subclassing is impractical (class explosion)
- You want to add/remove behavior at runtime
- You need to combine behaviors in various ways
Don't use Decorator when:
- The component interface is complex (many methods to delegate)
- You only need one fixed combination
- Order of decoration matters and is confusing
Related Patterns
- Composite: Decorator is a degenerate composite with one child
- Strategy: Changes the guts; Decorator changes the skin
- Proxy: Controls access; Decorator adds behavior
- Chain of Responsibility: Similar chaining, different intent
Identity Map Pattern
Intent
Ensure that each object gets loaded only once by keeping every loaded object in a map. Look up objects using the map when referring to them.
The Problem
Same entity loaded multiple times:
- Multiple instances of same database row
- Inconsistent state between instances
- Wasted database queries
- Update anomalies
// Without Identity Map - multiple instances of same entity
async function processOrder(orderId: string) {
// Load order
const order = await orderRepository.findById(orderId);
// Somewhere else, load same order
const orderAgain = await orderRepository.findById(orderId);
// These are different objects!
order.status = 'processing';
console.log(orderAgain.status); // Still 'pending' - inconsistent!
// Save order
await orderRepository.save(order);
// orderAgain is now stale
}The Solution
Maintain a map of loaded objects by their identity:
class IdentityMap<T extends { id: string }> {
private entities: Map<string, T> = new Map();
get(id: string): T | undefined {
return this.entities.get(id);
}
add(entity: T): void {
this.entities.set(entity.id, entity);
}
has(id: string): boolean {
return this.entities.has(id);
}
remove(id: string): void {
this.entities.delete(id);
}
clear(): void {
this.entities.clear();
}
}
// Repository with Identity Map
class UserRepository {
private identityMap = new IdentityMap<User>();
async findById(id: string): Promise<User | null> {
// Check identity map first
const cached = this.identityMap.get(id);
if (cached) {
return cached;
}
// Load from database
const row = await this.db.query('SELECT * FROM users WHERE id = $1', [id]);
if (!row) return null;
const user = this.mapToEntity(row);
this.identityMap.add(user);
return user;
}
async save(user: User): Promise<void> {
await this.db.query(
'UPDATE users SET name = $1, email = $2 WHERE id = $3',
[user.name, user.email, user.id]
);
// Entity already in map, no need to re-add
}
// Clear map at end of unit of work
clear(): void {
this.identityMap.clear();
}
private mapToEntity(row: any): User {
return new User(row.id, row.name, row.email);
}
}
// Now both references are the same object
async function processUser(userId: string) {
const user = await userRepository.findById(userId);
const userAgain = await userRepository.findById(userId);
console.log(user === userAgain); // true - same instance!
user.name = 'Updated';
console.log(userAgain.name); // 'Updated' - consistent!
}Generic Identity Map
type EntityType = string;
class GlobalIdentityMap {
private maps: Map<EntityType, Map<string, any>> = new Map();
private getMap(type: EntityType): Map<string, any> {
if (!this.maps.has(type)) {
this.maps.set(type, new Map());
}
return this.maps.get(type)!;
}
get<T>(type: EntityType, id: string): T | undefined {
return this.getMap(type).get(id);
}
add<T extends { id: string }>(type: EntityType, entity: T): void {
this.getMap(type).set(entity.id, entity);
}
has(type: EntityType, id: string): boolean {
return this.getMap(type).has(id);
}
remove(type: EntityType, id: string): void {
this.getMap(type).delete(id);
}
clearType(type: EntityType): void {
this.maps.delete(type);
}
clearAll(): void {
this.maps.clear();
}
}
// Base repository using global identity map
abstract class BaseRepository<T extends { id: string }> {
constructor(
protected db: Database,
protected identityMap: GlobalIdentityMap,
protected entityType: string
) {}
async findById(id: string): Promise<T | null> {
// Check identity map
const cached = this.identityMap.get<T>(this.entityType, id);
if (cached) return cached;
// Load from database
const entity = await this.loadFromDb(id);
if (entity) {
this.identityMap.add(this.entityType, entity);
}
return entity;
}
protected abstract loadFromDb(id: string): Promise<T | null>;
}
// Concrete repository
class OrderRepository extends BaseRepository<Order> {
constructor(db: Database, identityMap: GlobalIdentityMap) {
super(db, identityMap, 'Order');
}
protected async loadFromDb(id: string): Promise<Order | null> {
const row = await this.db.query('SELECT * FROM orders WHERE id = $1', [id]);
return row ? this.mapToEntity(row) : null;
}
}Request-Scoped Identity Map
// Identity map scoped to a request/transaction
class UnitOfWork {
private identityMaps: Map<string, IdentityMap<any>> = new Map();
private newEntities: Set<any> = new Set();
private dirtyEntities: Set<any> = new Set();
private deletedEntities: Set<any> = new Set();
getIdentityMap<T extends { id: string }>(type: string): IdentityMap<T> {
if (!this.identityMaps.has(type)) {
this.identityMaps.set(type, new IdentityMap<T>());
}
return this.identityMaps.get(type)!;
}
registerNew<T extends { id: string }>(type: string, entity: T): void {
this.getIdentityMap<T>(type).add(entity);
this.newEntities.add(entity);
}
registerDirty<T>(entity: T): void {
if (!this.newEntities.has(entity)) {
this.dirtyEntities.add(entity);
}
}
registerDeleted<T extends { id: string }>(type: string, entity: T): void {
this.getIdentityMap(type).remove(entity.id);
this.deletedEntities.add(entity);
this.dirtyEntities.delete(entity);
}
async commit(): Promise<void> {
// Insert new entities
for (const entity of this.newEntities) {
await this.insert(entity);
}
// Update dirty entities
for (const entity of this.dirtyEntities) {
await this.update(entity);
}
// Delete removed entities
for (const entity of this.deletedEntities) {
await this.delete(entity);
}
this.clear();
}
clear(): void {
this.identityMaps.clear();
this.newEntities.clear();
this.dirtyEntities.clear();
this.deletedEntities.clear();
}
private async insert(entity: any): Promise<void> { /* ... */ }
private async update(entity: any): Promise<void> { /* ... */ }
private async delete(entity: any): Promise<void> { /* ... */ }
}
// Express middleware to create request-scoped unit of work
function unitOfWorkMiddleware() {
return (req: Request, res: Response, next: NextFunction) => {
req.unitOfWork = new UnitOfWork();
// Commit on successful response
res.on('finish', async () => {
if (res.statusCode < 400) {
await req.unitOfWork.commit();
}
});
next();
};
}
// Repository uses request-scoped identity map
class UserRepository {
constructor(private db: Database) {}
async findById(id: string, uow: UnitOfWork): Promise<User | null> {
const identityMap = uow.getIdentityMap<User>('User');
const cached = identityMap.get(id);
if (cached) return cached;
const row = await this.db.query('SELECT * FROM users WHERE id = $1', [id]);
if (!row) return null;
const user = this.mapToEntity(row);
identityMap.add(user);
return user;
}
}With Change Tracking
class TrackedEntity<T extends { id: string }> {
private original: T;
private current: T;
private _isDirty = false;
constructor(entity: T) {
this.original = JSON.parse(JSON.stringify(entity));
this.current = entity;
}
get entity(): T {
return this.current;
}
get isDirty(): boolean {
return JSON.stringify(this.original) !== JSON.stringify(this.current);
}
markClean(): void {
this.original = JSON.parse(JSON.stringify(this.current));
}
}
class TrackingIdentityMap<T extends { id: string }> {
private tracked: Map<string, TrackedEntity<T>> = new Map();
get(id: string): T | undefined {
return this.tracked.get(id)?.entity;
}
add(entity: T): void {
this.tracked.set(entity.id, new TrackedEntity(entity));
}
getDirtyEntities(): T[] {
return Array.from(this.tracked.values())
.filter(t => t.isDirty)
.map(t => t.entity);
}
markAllClean(): void {
this.tracked.forEach(t => t.markClean());
}
remove(id: string): void {
this.tracked.delete(id);
}
clear(): void {
this.tracked.clear();
}
}
// Auto-detect dirty entities at commit
class AutoTrackingUnitOfWork {
private identityMap = new TrackingIdentityMap<any>();
async commit(): Promise<void> {
const dirtyEntities = this.identityMap.getDirtyEntities();
for (const entity of dirtyEntities) {
await this.update(entity);
}
this.identityMap.markAllClean();
}
}WeakMap-Based Identity Map
// For cases where you don't want to prevent garbage collection
class WeakIdentityMap {
private entityToId = new WeakMap<object, string>();
private idToEntity = new Map<string, WeakRef<object>>();
private registry = new FinalizationRegistry<string>((id) => {
this.idToEntity.delete(id);
});
get<T extends object>(id: string): T | undefined {
const ref = this.idToEntity.get(id);
return ref?.deref() as T | undefined;
}
add<T extends object & { id: string }>(entity: T): void {
this.entityToId.set(entity, entity.id);
this.idToEntity.set(entity.id, new WeakRef(entity));
this.registry.register(entity, entity.id);
}
has(id: string): boolean {
const ref = this.idToEntity.get(id);
return ref?.deref() !== undefined;
}
}Related Entity Handling
class OrderRepository {
constructor(
private db: Database,
private customerRepository: CustomerRepository,
private identityMap: GlobalIdentityMap
) {}
async findById(id: string): Promise<Order | null> {
// Check identity map
const cached = this.identityMap.get<Order>('Order', id);
if (cached) return cached;
// Load from database
const row = await this.db.query('SELECT * FROM orders WHERE id = $1', [id]);
if (!row) return null;
// Load related customer (also uses identity map)
const customer = await this.customerRepository.findById(row.customer_id);
const order = new Order(
row.id,
row.status,
row.total,
customer!
);
this.identityMap.add('Order', order);
return order;
}
}
// Both orders reference the same customer instance
const order1 = await orderRepository.findById('order-1');
const order2 = await orderRepository.findById('order-2');
// If both orders belong to same customer
console.log(order1.customer === order2.customer); // trueWhen to Use
Use Identity Map when:
- Same entity may be loaded multiple times
- You need consistent object identity
- Multiple parts of code reference same entity
- Using Unit of Work pattern
Don't use Identity Map when:
- Entities are immutable/read-only
- Each load should be fresh (no caching)
- Simple CRUD without relationships
- Memory constraints prevent caching
Related Patterns
- Unit of Work: Often uses Identity Map to track changes
- Repository: Implements Identity Map lookup
- Lazy Load: Works with Identity Map for related entities
- Data Mapper: Loads entities into Identity Map