Loading learning content...
The Chain of Responsibility pattern is one of the most widely applied behavioral patterns in production software. While the academic examples often focus on approval workflows, the pattern's real power becomes apparent across a stunning breadth of domains: HTTP middleware stacks, GUI event propagation, validation pipelines, logging systems, authentication mechanisms, and enterprise workflows.
This page explores substantial, production-quality implementations across these domains. Each example demonstrates not just the pattern structure, but the nuanced design decisions that make implementations robust, testable, and maintainable. These are the implementations you'll find in professional codebases.
By the end of this page, you will understand how to apply Chain of Responsibility to HTTP middleware, validation systems, logging frameworks, event handling, and enterprise approval workflows. You'll see complete implementations with error handling, observability, and edge case management. You'll also learn anti-patterns to avoid and how to recognize when the pattern is (and isn't) the right choice.
HTTP middleware is perhaps the most ubiquitous application of Chain of Responsibility in modern web development. Every Express.js, Koa, ASP.NET Core, or Django application uses this pattern for request processing. Understanding this implementation deeply informs how you build and debug web applications.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
/** * HTTP Middleware using Chain of Responsibility * * Key characteristics: * - Each middleware can modify request/response * - Chain can be short-circuited (e.g., auth failure) * - Bidirectional flow: before handling AND after response */ interface HttpRequest { method: string; path: string; headers: Record<string, string>; body?: unknown; context: Map<string, unknown>;} interface HttpResponse { status: number; headers: Record<string, string>; body?: unknown;} type MiddlewareResult = { handled: boolean; response?: HttpResponse;}; /** * Middleware handler with bidirectional processing */abstract class Middleware { private next: Middleware | null = null; setNext(middleware: Middleware): Middleware { this.next = middleware; return middleware; // Return next for chaining } async handle(request: HttpRequest): Promise<HttpResponse> { // Pre-processing (request phase) const preResult = await this.preProcess(request); if (preResult.handled) { return preResult.response!; } // Call next in chain let response: HttpResponse; if (this.next) { response = await this.next.handle(request); } else { // End of middleware chain - request reaches handler response = await this.handleRequest(request); } // Post-processing (response phase) return await this.postProcess(request, response); } /** * Pre-processing - runs BEFORE downstream middleware/handler. * Return handled=true to short-circuit the chain. */ protected async preProcess(request: HttpRequest): Promise<MiddlewareResult> { return { handled: false }; } /** * Post-processing - runs AFTER downstream completes. * Can modify the response. */ protected async postProcess( request: HttpRequest, response: HttpResponse ): Promise<HttpResponse> { return response; } /** * Terminal request handling (if this middleware is the final handler). */ protected async handleRequest(request: HttpRequest): Promise<HttpResponse> { return { status: 404, headers: {}, body: { error: 'Not Found' } }; }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
/** * Timing Middleware - measures request processing time */class TimingMiddleware extends Middleware { protected async preProcess(request: HttpRequest): Promise<MiddlewareResult> { request.context.set('startTime', Date.now()); return { handled: false }; } protected async postProcess( request: HttpRequest, response: HttpResponse ): Promise<HttpResponse> { const startTime = request.context.get('startTime') as number; const duration = Date.now() - startTime; // Add timing header response.headers['X-Response-Time'] = `${duration}ms`; // Log slow requests if (duration > 1000) { console.warn(`Slow request: ${request.method} ${request.path} took ${duration}ms`); } return response; }} /** * Authentication Middleware - validates tokens, short-circuits on failure */class AuthMiddleware extends Middleware { constructor(private readonly tokenValidator: TokenValidator) { super(); } protected async preProcess(request: HttpRequest): Promise<MiddlewareResult> { // Skip auth for public endpoints if (this.isPublicEndpoint(request.path)) { return { handled: false }; } const authHeader = request.headers['authorization']; if (!authHeader) { return { handled: true, response: { status: 401, headers: { 'WWW-Authenticate': 'Bearer' }, body: { error: 'Authentication required' }, }, }; } const token = authHeader.replace('Bearer ', ''); const user = await this.tokenValidator.validate(token); if (!user) { return { handled: true, response: { status: 401, headers: {}, body: { error: 'Invalid token' }, }, }; } // Add user to request context for downstream handlers request.context.set('user', user); return { handled: false }; } private isPublicEndpoint(path: string): boolean { const publicPaths = ['/health', '/login', '/register', '/public']; return publicPaths.some(p => path.startsWith(p)); }} /** * Rate Limiting Middleware - throttles excessive requests */class RateLimitMiddleware extends Middleware { private requestCounts: Map<string, { count: number; resetTime: number }> = new Map(); constructor( private readonly limit: number = 100, private readonly windowMs: number = 60000 ) { super(); } protected async preProcess(request: HttpRequest): Promise<MiddlewareResult> { const clientId = this.getClientId(request); const now = Date.now(); let record = this.requestCounts.get(clientId); if (!record || now > record.resetTime) { record = { count: 0, resetTime: now + this.windowMs }; this.requestCounts.set(clientId, record); } record.count++; if (record.count > this.limit) { return { handled: true, response: { status: 429, headers: { 'Retry-After': String(Math.ceil((record.resetTime - now) / 1000)), 'X-RateLimit-Limit': String(this.limit), 'X-RateLimit-Remaining': '0', }, body: { error: 'Too many requests' }, }, }; } return { handled: false }; } protected async postProcess( request: HttpRequest, response: HttpResponse ): Promise<HttpResponse> { const clientId = this.getClientId(request); const record = this.requestCounts.get(clientId); if (record) { response.headers['X-RateLimit-Limit'] = String(this.limit); response.headers['X-RateLimit-Remaining'] = String( Math.max(0, this.limit - record.count) ); } return response; } private getClientId(request: HttpRequest): string { return request.headers['x-forwarded-for'] || request.headers['x-client-id'] || 'anonymous'; }} /** * CORS Middleware - handles cross-origin requests */class CorsMiddleware extends Middleware { constructor(private readonly allowedOrigins: string[]) { super(); } protected async preProcess(request: HttpRequest): Promise<MiddlewareResult> { // Handle preflight requests if (request.method === 'OPTIONS') { return { handled: true, response: { status: 204, headers: this.getCorsHeaders(request), body: undefined, }, }; } return { handled: false }; } protected async postProcess( request: HttpRequest, response: HttpResponse ): Promise<HttpResponse> { // Add CORS headers to all responses const corsHeaders = this.getCorsHeaders(request); Object.assign(response.headers, corsHeaders); return response; } private getCorsHeaders(request: HttpRequest): Record<string, string> { const origin = request.headers['origin']; const allowedOrigin = this.allowedOrigins.includes(origin) ? origin : this.allowedOrigins[0]; return { 'Access-Control-Allow-Origin': allowedOrigin, 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 'Access-Control-Max-Age': '86400', }; }}12345678910111213141516171819202122232425262728293031323334
// Building the middleware pipelineconst timing = new TimingMiddleware();const rateLimit = new RateLimitMiddleware(100, 60000);const cors = new CorsMiddleware(['https://app.example.com']);const auth = new AuthMiddleware(new TokenValidator());const router = new RouterMiddleware(routes); // Final handler // Chain assembly - order matters!timing .setNext(rateLimit) .setNext(cors) .setNext(auth) .setNext(router); // Request processingasync function handleHttp(rawRequest: IncomingMessage): Promise<OutgoingMessage> { const request = parseRequest(rawRequest); // Send through middleware chain const response = await timing.handle(request); return formatResponse(response);} // Execution flow for authenticated request:// 1. TimingMiddleware.preProcess → records start time// 2. RateLimitMiddleware.preProcess → checks rate limit// 3. CorsMiddleware.preProcess → handles OPTIONS (if preflight)// 4. AuthMiddleware.preProcess → validates token, adds user// 5. RouterMiddleware handles request → produces response// 6. AuthMiddleware.postProcess → (no-op)// 7. CorsMiddleware.postProcess → adds CORS headers// 8. RateLimitMiddleware.postProcess → adds rate limit headers// 9. TimingMiddleware.postProcess → adds timing headerNotice the bidirectional nature of this middleware implementation. Each middleware has both pre-processing (request phase) and post-processing (response phase). This enables powerful patterns like timing (start in pre, measure in post), transaction management (begin in pre, commit/rollback in post), and context propagation.
Validation is another natural fit for Chain of Responsibility. Each validator checks one aspect of the data, and the chain can either collect all errors (aggregate mode) or stop at the first error (fail-fast mode).
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
/** * Validation Chain with configurable behavior: * - Fail-fast mode: stop at first error * - Aggregate mode: collect all errors */ interface ValidationError { field: string; code: string; message: string;} interface ValidationResult { valid: boolean; errors: ValidationError[];} interface ValidationContext<T> { data: T; errors: ValidationError[]; stopOnFirstError: boolean;} abstract class Validator<T> { private next: Validator<T> | null = null; setNext(validator: Validator<T>): Validator<T> { this.next = validator; return validator; } validate(context: ValidationContext<T>): ValidationResult { // Run this validator const errors = this.doValidate(context.data); context.errors.push(...errors); // Check if we should stop if (context.stopOnFirstError && errors.length > 0) { return { valid: false, errors: context.errors }; } // Continue to next validator if (this.next) { return this.next.validate(context); } // End of chain - return accumulated result return { valid: context.errors.length === 0, errors: context.errors, }; } protected abstract doValidate(data: T): ValidationError[];} /** * Factory function to run validation chain */function runValidation<T>( data: T, chain: Validator<T>, stopOnFirstError: boolean = true): ValidationResult { const context: ValidationContext<T> = { data, errors: [], stopOnFirstError, }; return chain.validate(context);}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
interface UserRegistration { email: string; password: string; username: string; dateOfBirth: string; country: string;} /** * Email Validator - checks format and domain */class EmailValidator extends Validator<UserRegistration> { private readonly blockedDomains = ['tempmail.com', 'throwaway.com']; protected doValidate(data: UserRegistration): ValidationError[] { const errors: ValidationError[] = []; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!data.email) { errors.push({ field: 'email', code: 'REQUIRED', message: 'Email is required', }); } else if (!emailRegex.test(data.email)) { errors.push({ field: 'email', code: 'INVALID_FORMAT', message: 'Email format is invalid', }); } else { const domain = data.email.split('@')[1]; if (this.blockedDomains.includes(domain)) { errors.push({ field: 'email', code: 'BLOCKED_DOMAIN', message: 'Temporary email addresses are not allowed', }); } } return errors; }} /** * Password Validator - checks strength requirements */class PasswordValidator extends Validator<UserRegistration> { protected doValidate(data: UserRegistration): ValidationError[] { const errors: ValidationError[] = []; const password = data.password || ''; if (password.length < 8) { errors.push({ field: 'password', code: 'TOO_SHORT', message: 'Password must be at least 8 characters', }); } if (!/[A-Z]/.test(password)) { errors.push({ field: 'password', code: 'NO_UPPERCASE', message: 'Password must contain at least one uppercase letter', }); } if (!/[a-z]/.test(password)) { errors.push({ field: 'password', code: 'NO_LOWERCASE', message: 'Password must contain at least one lowercase letter', }); } if (!/[0-9]/.test(password)) { errors.push({ field: 'password', code: 'NO_NUMBER', message: 'Password must contain at least one number', }); } if (!/[!@#$%^&*]/.test(password)) { errors.push({ field: 'password', code: 'NO_SPECIAL', message: 'Password must contain at least one special character', }); } return errors; }} /** * Username Validator - checks format and availability */class UsernameValidator extends Validator<UserRegistration> { constructor(private readonly userService: UserService) { super(); } protected doValidate(data: UserRegistration): ValidationError[] { const errors: ValidationError[] = []; const username = data.username || ''; if (username.length < 3) { errors.push({ field: 'username', code: 'TOO_SHORT', message: 'Username must be at least 3 characters', }); } else if (username.length > 20) { errors.push({ field: 'username', code: 'TOO_LONG', message: 'Username must be at most 20 characters', }); } else if (!/^[a-zA-Z0-9_]+$/.test(username)) { errors.push({ field: 'username', code: 'INVALID_CHARACTERS', message: 'Username can only contain letters, numbers, and underscores', }); } else if (this.userService.usernameExists(username)) { errors.push({ field: 'username', code: 'ALREADY_EXISTS', message: 'Username is already taken', }); } return errors; }} /** * Age Validator - checks minimum age requirements */class AgeValidator extends Validator<UserRegistration> { constructor(private readonly minimumAge: number = 13) { super(); } protected doValidate(data: UserRegistration): ValidationError[] { const errors: ValidationError[] = []; const birthDate = new Date(data.dateOfBirth); if (isNaN(birthDate.getTime())) { errors.push({ field: 'dateOfBirth', code: 'INVALID_DATE', message: 'Date of birth is invalid', }); } else { const age = this.calculateAge(birthDate); if (age < this.minimumAge) { errors.push({ field: 'dateOfBirth', code: 'UNDER_AGE', message: `You must be at least ${this.minimumAge} years old to register`, }); } } return errors; } private calculateAge(birthDate: Date): number { const today = new Date(); let age = today.getFullYear() - birthDate.getFullYear(); const monthDiff = today.getMonth() - birthDate.getMonth(); if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { age--; } return age; }}12345678910111213141516171819202122232425262728293031323334353637383940414243
// Build validation chainconst emailValidator = new EmailValidator();const passwordValidator = new PasswordValidator();const usernameValidator = new UsernameValidator(userService);const ageValidator = new AgeValidator(13); emailValidator .setNext(passwordValidator) .setNext(usernameValidator) .setNext(ageValidator); // Usage in registration handlerasync function registerUser(data: UserRegistration): Promise<RegistrationResponse> { // Aggregate mode - collect all errors const validationResult = runValidation(data, emailValidator, false); if (!validationResult.valid) { return { success: false, errors: validationResult.errors, }; } // Proceed with registration const user = await userService.create(data); return { success: true, userId: user.id };} // Example with invalid dataconst invalidData: UserRegistration = { email: 'invalid-email', password: 'weak', username: 'ab', dateOfBirth: '2015-01-01', // Too young country: 'US',}; const result = runValidation(invalidData, emailValidator, false);// result.errors contains:// - email: INVALID_FORMAT// - password: TOO_SHORT, NO_UPPERCASE, NO_NUMBER, NO_SPECIAL// - username: TOO_SHORT// - dateOfBirth: UNDER_AGELogging systems often use Chain of Responsibility to route logs to multiple destinations (console, file, network) and apply filtering. Each handler in the chain can process and/or pass the log entry onwards.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
/** * Logging with Chain of Responsibility * * Features: * - Multiple log destinations (console, file, network) * - Level-based filtering per handler * - All handlers can process (propagate-through-all strategy) */ enum LogLevel { DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3, FATAL = 4,} interface LogEntry { level: LogLevel; message: string; timestamp: Date; context?: Record<string, unknown>; error?: Error;} abstract class LogHandler { private next: LogHandler | null = null; protected minLevel: LogLevel = LogLevel.DEBUG; setNext(handler: LogHandler): LogHandler { this.next = handler; return handler; } setLevel(level: LogLevel): this { this.minLevel = level; return this; } log(entry: LogEntry): void { // Process if level meets threshold if (entry.level >= this.minLevel) { this.write(entry); } // ALWAYS forward to next (propagate-through-all) if (this.next) { this.next.log(entry); } } protected abstract write(entry: LogEntry): void; protected formatEntry(entry: LogEntry): string { const level = LogLevel[entry.level]; const time = entry.timestamp.toISOString(); let message = `[${time}] [${level}] ${entry.message}`; if (entry.context) { message += ` ${JSON.stringify(entry.context)}`; } if (entry.error) { message += `${entry.error.stack}`; } return message; }} /** * Console Logger - writes to stdout/stderr */class ConsoleLogHandler extends LogHandler { protected write(entry: LogEntry): void { const formatted = this.formatEntry(entry); if (entry.level >= LogLevel.ERROR) { console.error(formatted); } else { console.log(formatted); } }} /** * File Logger - appends to log file */class FileLogHandler extends LogHandler { constructor(private readonly filePath: string) { super(); } protected write(entry: LogEntry): void { const formatted = this.formatEntry(entry) + ''; // In real implementation: use async file writing with buffering fs.appendFileSync(this.filePath, formatted); }} /** * Network Logger - sends to log aggregation service */class NetworkLogHandler extends LogHandler { private buffer: LogEntry[] = []; private readonly flushInterval: number = 5000; private readonly batchSize: number = 100; constructor(private readonly endpoint: string) { super(); // Flush periodically setInterval(() => this.flush(), this.flushInterval); } protected write(entry: LogEntry): void { this.buffer.push(entry); if (this.buffer.length >= this.batchSize) { this.flush(); } } private async flush(): Promise<void> { if (this.buffer.length === 0) return; const batch = this.buffer; this.buffer = []; try { await fetch(this.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(batch), }); } catch (error) { // Re-add failed entries (with limit to prevent memory issues) if (this.buffer.length < 1000) { this.buffer.unshift(...batch); } } }} /** * Alert Logger - sends alerts for critical logs */class AlertLogHandler extends LogHandler { constructor(private readonly alertService: AlertService) { super(); this.minLevel = LogLevel.ERROR; // Only alert on errors } protected write(entry: LogEntry): void { const severity = entry.level === LogLevel.FATAL ? 'critical' : 'high'; this.alertService.sendAlert({ title: `[${LogLevel[entry.level]}] ${entry.message}`, severity, context: entry.context, error: entry.error?.stack, }); }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
// Build logging chain for productionconst consoleLogger = new ConsoleLogHandler().setLevel(LogLevel.INFO);const fileLogger = new FileLogHandler('/var/log/app.log').setLevel(LogLevel.DEBUG);const networkLogger = new NetworkLogHandler('https://logs.example.com/ingest').setLevel(LogLevel.INFO);const alertLogger = new AlertLogHandler(alertService).setLevel(LogLevel.ERROR); // Chain: all logs flow through all handlers (each filters by level)consoleLogger .setNext(fileLogger) .setNext(networkLogger) .setNext(alertLogger); // Logger facadeclass Logger { constructor(private readonly chain: LogHandler) {} debug(message: string, context?: Record<string, unknown>): void { this.chain.log({ level: LogLevel.DEBUG, message, timestamp: new Date(), context }); } info(message: string, context?: Record<string, unknown>): void { this.chain.log({ level: LogLevel.INFO, message, timestamp: new Date(), context }); } warn(message: string, context?: Record<string, unknown>): void { this.chain.log({ level: LogLevel.WARN, message, timestamp: new Date(), context }); } error(message: string, error?: Error, context?: Record<string, unknown>): void { this.chain.log({ level: LogLevel.ERROR, message, timestamp: new Date(), context, error }); } fatal(message: string, error?: Error, context?: Record<string, unknown>): void { this.chain.log({ level: LogLevel.FATAL, message, timestamp: new Date(), context, error }); }} // Usageconst logger = new Logger(consoleLogger); logger.info('Application started', { version: '1.0.0' });// → Console: INFO log// → File: INFO log// → Network: INFO log// → Alert: (skipped - level < ERROR) logger.error('Database connection failed', new Error('Connection refused'), { host: 'db.example.com' });// → Console: ERROR log// → File: ERROR log// → Network: ERROR log// → Alert: Alert sent!GUI frameworks use Chain of Responsibility for event propagation. Events bubble up through the component hierarchy until handled. This allows components to handle events at the appropriate level without global event registration.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
/** * UI Event Handling with Chain of Responsibility * * Implements: * - Event bubbling (child → parent) * - Event capturing (parent → child) * - Stop propagation */ interface UIEvent { type: 'click' | 'key' | 'focus' | 'submit' | 'custom'; target: UIComponent; data?: unknown; stopped: boolean; timestamp: Date;} abstract class UIComponent { private parent: UIComponent | null = null; private children: UIComponent[] = []; private eventHandlers: Map<string, ((event: UIEvent) => void)[]> = new Map(); constructor(public readonly id: string) {} addChild(child: UIComponent): void { child.parent = this; this.children.push(child); } removeChild(child: UIComponent): void { const index = this.children.indexOf(child); if (index !== -1) { child.parent = null; this.children.splice(index, 1); } } /** * Register an event handler */ on(eventType: string, handler: (event: UIEvent) => void): void { if (!this.eventHandlers.has(eventType)) { this.eventHandlers.set(eventType, []); } this.eventHandlers.get(eventType)!.push(handler); } /** * Trigger an event - it bubbles up through the chain */ triggerEvent(event: UIEvent): void { // Handle at this component this.handleEvent(event); // Bubble up to parent (unless stopped) if (!event.stopped && this.parent) { this.parent.triggerEvent(event); } } private handleEvent(event: UIEvent): void { const handlers = this.eventHandlers.get(event.type); if (handlers) { for (const handler of handlers) { handler(event); if (event.stopped) break; } } } /** * Find a child component by ID */ findById(id: string): UIComponent | null { if (this.id === id) return this; for (const child of this.children) { const found = child.findById(id); if (found) return found; } return null; }} /** * Concrete UI Components */class Window extends UIComponent { constructor() { super('window'); // Window handles unhandled events this.on('click', (event) => { console.log(`Window received click (from ${event.target.id})`); }); this.on('key', (event) => { const keyData = event.data as { key: string }; if (keyData.key === 'Escape') { console.log('Window handling Escape - closing dialogs'); event.stopped = true; } }); }} class Panel extends UIComponent { constructor(id: string) { super(id); }} class Button extends UIComponent { constructor(id: string, private readonly label: string) { super(id); } click(): void { const event: UIEvent = { type: 'click', target: this, stopped: false, timestamp: new Date(), }; this.triggerEvent(event); }} class TextField extends UIComponent { private value: string = ''; constructor(id: string) { super(id); } focus(): void { const event: UIEvent = { type: 'focus', target: this, stopped: false, timestamp: new Date(), }; this.triggerEvent(event); } keyPress(key: string): void { const event: UIEvent = { type: 'key', target: this, data: { key }, stopped: false, timestamp: new Date(), }; this.triggerEvent(event); }}12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// Build component hierarchyconst window = new Window();const mainPanel = new Panel('main-panel');const toolbar = new Panel('toolbar');const saveButton = new Button('save-btn', 'Save');const searchField = new TextField('search-field'); window.addChild(mainPanel);mainPanel.addChild(toolbar);toolbar.addChild(saveButton);toolbar.addChild(searchField); // Component hierarchy:// Window// └─ MainPanel// └─ Toolbar// ├─ SaveButton// └─ SearchField // Register handlers at different levelssaveButton.on('click', (event) => { console.log('Save button clicked - saving document'); event.stopped = true; // Stop propagation}); toolbar.on('click', (event) => { console.log('Toolbar received click'); // This won't run for saveButton clicks (stopped above)}); mainPanel.on('focus', (event) => { console.log(`Focus entered panel from ${event.target.id}`);}); // Trigger eventssaveButton.click();// Output: "Save button clicked - saving document"// (propagation stopped - toolbar/window don't receive) searchField.focus();// Output: "Focus entered panel from search-field"// (bubbles through toolbar → mainPanel) searchField.keyPress('Escape');// Output: "Window handling Escape - closing dialogs"// (bubbles all the way to window, which handles Escape)Real UI frameworks often implement three phases: capture (top-down), target (at the target component), and bubble (bottom-up). The example shows bubbling. Adding capture phase requires reversing the chain traversal order during the capture phase before proceeding to bubble phase.
Let's conclude with a comprehensive enterprise approval workflow that combines multiple concepts: conditional routing, audit logging, SLA tracking, and escalation management.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
/** * Enterprise Expense Approval System * * Features: * - Multi-level approval based on amount and category * - Audit trail for compliance * - SLA tracking for escalation * - Policy validation */ interface ExpenseRequest { id: string; amount: number; currency: string; category: 'TRAVEL' | 'EQUIPMENT' | 'SOFTWARE' | 'CONTRACTOR' | 'OTHER'; description: string; requester: Employee; attachments: string[]; submittedAt: Date; metadata: Record<string, unknown>;} interface Employee { id: string; name: string; department: string; level: number; // 1=IC, 2=Manager, 3=Director, 4=VP, 5=C-Suite managerId: string | null;} interface ApprovalDecision { approver: Employee; decision: 'APPROVED' | 'REJECTED' | 'ESCALATED' | 'PENDING'; comments?: string; conditions?: string[]; decidedAt: Date;} interface ApprovalResult { request: ExpenseRequest; decisions: ApprovalDecision[]; finalStatus: 'APPROVED' | 'REJECTED' | 'PENDING'; totalProcessingTime: number;} /** * Abstract Approval Handler with audit and SLA support */abstract class ApprovalHandler { private next: ApprovalHandler | null = null; constructor( protected readonly auditLog: AuditLog, protected readonly slaConfig: { maxHours: number } ) {} setNext(handler: ApprovalHandler): ApprovalHandler { this.next = handler; return handler; } async process( request: ExpenseRequest, decisions: ApprovalDecision[] ): Promise<ApprovalResult> { const startTime = Date.now(); // Log entry await this.auditLog.log({ action: 'APPROVAL_STARTED', handler: this.constructor.name, requestId: request.id, timestamp: new Date(), }); // Check if this handler should process const shouldProcess = await this.shouldHandle(request); if (shouldProcess) { const decision = await this.makeDecision(request); decisions.push(decision); // Log decision await this.auditLog.log({ action: 'DECISION_MADE', handler: this.constructor.name, requestId: request.id, decision: decision.decision, timestamp: new Date(), }); // If rejected or approved and no further approval needed if (decision.decision === 'REJECTED') { return this.createResult(request, decisions, 'REJECTED', startTime); } if (decision.decision === 'APPROVED' && !this.requiresFurtherApproval(request)) { return this.createResult(request, decisions, 'APPROVED', startTime); } } // Forward to next handler if (this.next) { return this.next.process(request, decisions); } // End of chain - determine final status const finalStatus = this.determineFinalStatus(decisions); return this.createResult(request, decisions, finalStatus, startTime); } protected abstract shouldHandle(request: ExpenseRequest): Promise<boolean>; protected abstract makeDecision(request: ExpenseRequest): Promise<ApprovalDecision>; protected requiresFurtherApproval(request: ExpenseRequest): boolean { return false; // Override if needed } private determineFinalStatus(decisions: ApprovalDecision[]): 'APPROVED' | 'REJECTED' | 'PENDING' { if (decisions.some(d => d.decision === 'REJECTED')) return 'REJECTED'; if (decisions.every(d => d.decision === 'APPROVED')) return 'APPROVED'; return 'PENDING'; } private createResult( request: ExpenseRequest, decisions: ApprovalDecision[], status: 'APPROVED' | 'REJECTED' | 'PENDING', startTime: number ): ApprovalResult { return { request, decisions, finalStatus: status, totalProcessingTime: Date.now() - startTime, }; }} /** * Policy Validation Handler - first in chain */class PolicyValidationHandler extends ApprovalHandler { constructor( private readonly policyService: PolicyService, auditLog: AuditLog ) { super(auditLog, { maxHours: 1 }); } protected async shouldHandle(): Promise<boolean> { return true; // Always validate policy } protected async makeDecision(request: ExpenseRequest): Promise<ApprovalDecision> { const violations = await this.policyService.checkViolations(request); if (violations.length > 0) { return { approver: { id: 'SYSTEM', name: 'Policy Engine' } as Employee, decision: 'REJECTED', comments: `Policy violations: ${violations.join(', ')}`, decidedAt: new Date(), }; } return { approver: { id: 'SYSTEM', name: 'Policy Engine' } as Employee, decision: 'APPROVED', comments: 'Passed policy validation', decidedAt: new Date(), }; } protected requiresFurtherApproval(): boolean { return true; // Policy is just the first check }} /** * Manager Approval Handler */class ManagerApprovalHandler extends ApprovalHandler { constructor( private readonly employeeService: EmployeeService, private readonly approvalLimit: number, auditLog: AuditLog ) { super(auditLog, { maxHours: 24 }); } protected async shouldHandle(request: ExpenseRequest): Promise<boolean> { return request.amount <= this.approvalLimit; } protected async makeDecision(request: ExpenseRequest): Promise<ApprovalDecision> { const manager = await this.employeeService.getManager(request.requester.id); // In real system, this would wait for manager's actual decision // Here we simulate auto-approval for demonstration return { approver: manager, decision: 'APPROVED', comments: 'Approved by direct manager', decidedAt: new Date(), }; }} /** * Director Approval Handler */class DirectorApprovalHandler extends ApprovalHandler { constructor( private readonly employeeService: EmployeeService, private readonly minAmount: number, private readonly maxAmount: number, auditLog: AuditLog ) { super(auditLog, { maxHours: 48 }); } protected async shouldHandle(request: ExpenseRequest): Promise<boolean> { return request.amount > this.minAmount && request.amount <= this.maxAmount; } protected async makeDecision(request: ExpenseRequest): Promise<ApprovalDecision> { const director = await this.employeeService.getDepartmentDirector( request.requester.department ); return { approver: director, decision: 'APPROVED', comments: 'Approved by department director', conditions: ['Requires receipt within 30 days'], decidedAt: new Date(), }; }} /** * Finance Review Handler - for large expenses */class FinanceReviewHandler extends ApprovalHandler { constructor( private readonly threshold: number, auditLog: AuditLog ) { super(auditLog, { maxHours: 72 }); } protected async shouldHandle(request: ExpenseRequest): Promise<boolean> { return request.amount > this.threshold; } protected async makeDecision(request: ExpenseRequest): Promise<ApprovalDecision> { // Finance review includes budget check return { approver: { id: 'FINANCE', name: 'Finance Department' } as Employee, decision: 'APPROVED', comments: 'Budget verified and approved by Finance', decidedAt: new Date(), }; }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
// Build approval chainfunction createApprovalWorkflow( policyService: PolicyService, employeeService: EmployeeService, auditLog: AuditLog): ApprovalHandler { const policy = new PolicyValidationHandler(policyService, auditLog); const manager = new ManagerApprovalHandler(employeeService, 5000, auditLog); const director = new DirectorApprovalHandler(employeeService, 5000, 25000, auditLog); const finance = new FinanceReviewHandler(25000, auditLog); policy .setNext(manager) .setNext(director) .setNext(finance); return policy;} // Usageconst workflow = createApprovalWorkflow(policyService, employeeService, auditLog); async function submitExpense(request: ExpenseRequest): Promise<ApprovalResult> { console.log(`Processing expense request ${request.id} for ${request.amount} ${request.currency}`); const result = await workflow.process(request, []); console.log(`Request ${request.id}: ${result.finalStatus}`); console.log(`Decisions: ${result.decisions.length}`); result.decisions.forEach(d => { console.log(` - ${d.approver.name}: ${d.decision}`); }); return result;} // Example: Small expense (≤$5000) - Manager handlesawait submitExpense({ id: 'EXP-001', amount: 500, currency: 'USD', category: 'TRAVEL', description: 'Client meeting transportation', requester: employee, attachments: [], submittedAt: new Date(), metadata: {},});// Flow: Policy ✓ → Manager ✓ → Done // Example: Large expense (>$25000) - Full chainawait submitExpense({ id: 'EXP-002', amount: 50000, currency: 'USD', category: 'EQUIPMENT', description: 'Server infrastructure upgrade', requester: employee, attachments: ['quote.pdf'], submittedAt: new Date(), metadata: {},});// Flow: Policy ✓ → Manager (skipped) → Director (skipped) → Finance ✓ → DoneWhile Chain of Responsibility is powerful, misapplication leads to problematic designs. Recognizing these anti-patterns helps you apply the pattern effectively.
123456789101112131415161718192021222324252627
// ❌ BAD: Handler knows about other handlersclass BadHandler extends AbstractHandler<Request, Response> { handle(request: Request): Response | null { if (this.shouldHandle(request)) { return this.processRequest(request); } // Anti-pattern: Handler knows specific successor if (request.type === 'SPECIAL') { // Skip to SpecialHandler directly - breaks chain independence! const specialHandler = new SpecialHandler(); return specialHandler.handle(request); } return this.forward(request); }} // ✅ GOOD: Handler is independentclass GoodHandler extends AbstractHandler<Request, Response> { handle(request: Request): Response | null { if (this.shouldHandle(request)) { return this.processRequest(request); } return this.forward(request); // Always use chain }}The pattern isn't always appropriate. Avoid it when: simple conditionals suffice (one or two handlers), order doesn't matter (use Observer instead), all handlers must execute (simple iteration may be clearer), or performance is critical (chain traversal adds overhead). Choose patterns based on problem fit, not pattern enthusiasm.
We've explored diverse real-world applications of the Chain of Responsibility pattern. Let's consolidate the key insights:
| Use Case | Chain Strategy | Key Feature |
|---|---|---|
| HTTP Middleware | Propagate-through-all | Bidirectional processing (request + response phases) |
| Validation | Fail-fast or Aggregate | Configurable error collection |
| Logging | Propagate-through-all | Level-based filtering per handler |
| Event Handling | Stop-at-first | Event bubbling through hierarchy |
| Approval Workflow | Conditional routing | Audit trail and SLA tracking |
You've completed the Chain of Responsibility pattern module. You understand the problem it solves, how to structure the solution, techniques for chain construction, and diverse real-world applications. You're now equipped to apply this pattern effectively in your own projects—and to recognize it in the frameworks and libraries you use daily.