Loading content...
Understanding the Chain of Responsibility pattern structure is the first step. But in production systems, how you construct and manage chains determines whether the pattern delivers its promised benefits or becomes another source of complexity.
Chain construction isn't just about linking handlers—it's about creating flexible, configurable pipelines that can adapt to different contexts, support testing, and evolve with requirements. Poor chain construction reintroduces the coupling we worked to eliminate; excellent chain construction enables the loose coupling, testability, and extensibility the pattern promises.
This page explores chain construction from first principles through advanced patterns used in production systems.
By the end of this page, you will master multiple chain construction approaches: manual assembly, factory patterns, builder patterns, and dependency injection. You'll understand dynamic chain modification, context-aware chain selection, and lifecycle management. You'll also learn how to test chain construction separately from handler logic.
Before exploring advanced patterns, let's establish the fundamental principles that guide chain construction:
The chain construction responsibility:
Chain construction is a distinct responsibility that should be separated from both handler implementation and request processing. This separation provides several benefits:
| Responsibility | Owner | Should NOT Own |
|---|---|---|
| Handler logic | Concrete Handler classes | Chain structure, ordering, other handlers |
| Chain structure | Chain factory/builder | Handler implementation details |
| Chain usage | Client code | Chain structure, handler details |
| Handler instantiation | DI container or factory | Business logic, chain ordering |
1234567891011121314151617181920212223
// Manual chain assembly - the baseline approach function createSimpleChain(): Handler<Request, Response> { // Create handlers const auth = new AuthHandler(); const validation = new ValidationHandler(); const processing = new ProcessingHandler(); const logging = new LoggingHandler(); // Link handlers auth.setNext(validation); validation.setNext(processing); processing.setNext(logging); // Return chain head return auth;} // Issues with this approach:// 1. Chain structure is hardcoded// 2. No way to change order without code changes// 3. Handlers are instantiated directly - no injection// 4. No way to conditionally include/exclude handlersChain of Responsibility decouples handlers from each other, but something must know the chain structure to assemble it. This isn't a design flaw—it's the pattern working correctly. The key insight is that chain construction is a separate, isolated concern that should be concentrated in one place (factory, builder, or configuration) rather than spread throughout the codebase.
The Factory Pattern applied to chain construction centralizes chain assembly logic and provides a clean abstraction for chain creation. This approach is ideal when you have multiple predefined chain configurations.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
/** * Chain Factory - encapsulates chain construction logic * * Benefits: * - Centralized chain construction * - Named chain configurations * - Easy to test chain assembly separately * - Supports environment-specific chains */class ApprovalChainFactory { /** * Creates the standard approval chain for production use. */ static createStandardChain(): Handler<PurchaseOrder, ApprovalResult> { const supervisor = new SupervisorHandler(); const manager = new ManagerHandler(); const director = new DirectorHandler(); const ceo = new CEOHandler(); const fallback = new FallbackHandler(); return supervisor .setNext(manager) .setNext(director) .setNext(ceo) .setNext(fallback) as Handler<PurchaseOrder, ApprovalResult>; } /** * Creates a simplified chain for testing purposes. * Only includes supervisor and fallback. */ static createTestChain(): Handler<PurchaseOrder, ApprovalResult> { const supervisor = new SupervisorHandler(); const fallback = new FallbackHandler(); return supervisor.setNext(fallback) as Handler<PurchaseOrder, ApprovalResult>; } /** * Creates an expedited chain for urgent orders. * Skips supervisor/manager - goes directly to director. */ static createExpediteChain(): Handler<PurchaseOrder, ApprovalResult> { const director = new DirectorHandler(); const ceo = new CEOHandler(); const fallback = new FallbackHandler(); return director .setNext(ceo) .setNext(fallback) as Handler<PurchaseOrder, ApprovalResult>; } /** * Creates chain based on environment configuration. */ static createFromEnvironment(): Handler<PurchaseOrder, ApprovalResult> { const env = process.env.NODE_ENV; switch (env) { case 'test': return this.createTestChain(); case 'development': return this.createTestChain(); default: return this.createStandardChain(); } }} // Usageconst chain = ApprovalChainFactory.createStandardChain();const processor = new PurchaseOrderProcessor(chain);When to use factory-based construction:
For chain construction, the Factory Method pattern (static factory methods as shown) is usually sufficient. Use Abstract Factory when you need to create families of related chains (e.g., different handler implementations for different regions) while ensuring consistency within each family.
When chain construction requires more flexibility—conditional handlers, parameterized handlers, or complex composition rules—the Builder Pattern provides superior expressiveness.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
/** * Chain Builder - fluent builder for flexible chain construction * * Benefits over Factory: * - Dynamic composition based on runtime conditions * - Fluent API for readable construction * - Easy to add/skip handlers conditionally * - Supports handler parameterization */class ApprovalChainBuilder<T, R> { private handlers: Handler<T, R>[] = []; /** * Adds a handler to the chain. */ addHandler(handler: Handler<T, R>): this { this.handlers.push(handler); return this; } /** * Conditionally adds a handler based on predicate. */ addHandlerIf(condition: boolean, handler: Handler<T, R>): this { if (condition) { this.handlers.push(handler); } return this; } /** * Conditionally adds a handler using lazy evaluation. * Handler is only instantiated if condition is true. */ addHandlerIfLazy( condition: boolean, handlerFactory: () => Handler<T, R> ): this { if (condition) { this.handlers.push(handlerFactory()); } return this; } /** * Adds multiple handlers at once. */ addHandlers(...handlers: Handler<T, R>[]): this { this.handlers.push(...handlers); return this; } /** * Builds the chain by linking all handlers. * Returns the first handler (chain head). */ build(): Handler<T, R> { if (this.handlers.length === 0) { throw new Error('Cannot build empty chain'); } // Link handlers together for (let i = 0; i < this.handlers.length - 1; i++) { this.handlers[i].setNext(this.handlers[i + 1]); } return this.handlers[0]; } /** * Returns the number of handlers that will be in the chain. */ size(): number { return this.handlers.length; } /** * Resets the builder for reuse. */ reset(): this { this.handlers = []; return this; }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
// Flexible chain construction based on configuration interface ChainConfig { includeRegionalApproval: boolean; includeLegalReview: boolean; expedited: boolean; maxApprovalLevel: 'supervisor' | 'manager' | 'director' | 'ceo';} function buildApprovalChain(config: ChainConfig): Handler<PurchaseOrder, ApprovalResult> { const builder = new ApprovalChainBuilder<PurchaseOrder, ApprovalResult>(); // Conditionally add regional approval builder.addHandlerIf( config.includeRegionalApproval, new RegionalApprovalHandler() ); // Conditionally add legal review builder.addHandlerIf( config.includeLegalReview, new LegalReviewHandler() ); // Add approval chain based on max level if (!config.expedited) { builder.addHandler(new SupervisorHandler()); } if (config.maxApprovalLevel !== 'supervisor') { builder.addHandler(new ManagerHandler()); } if (['director', 'ceo'].includes(config.maxApprovalLevel)) { builder.addHandler(new DirectorHandler()); } if (config.maxApprovalLevel === 'ceo') { builder.addHandler(new CEOHandler()); } // Always add fallback builder.addHandler(new FallbackHandler()); console.log(`Built chain with ${builder.size()} handlers`); return builder.build();} // Usage examplesconst standardChain = buildApprovalChain({ includeRegionalApproval: false, includeLegalReview: false, expedited: false, maxApprovalLevel: 'ceo',}); const internationalChain = buildApprovalChain({ includeRegionalApproval: true, includeLegalReview: true, expedited: false, maxApprovalLevel: 'ceo',}); const expeditedChain = buildApprovalChain({ includeRegionalApproval: false, includeLegalReview: false, expedited: true, maxApprovalLevel: 'director',});Use Factory when you have fixed, named configurations and value simplicity. Use Builder when chain composition is dynamic, configuration-driven, or requires complex conditional logic. In practice, you might use both: a Factory that internally uses a Builder for complex chains.
In production applications, Dependency Injection (DI) is the preferred approach for managing handler instantiation and chain construction. DI enables:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
/** * Handler with injected dependencies */class ComplianceHandler extends AbstractHandler<PurchaseOrder, ApprovalResult> { constructor( private readonly complianceService: ComplianceService, private readonly auditLogger: AuditLogger ) { super(); } handle(order: PurchaseOrder): ApprovalResult | null { const isCompliant = this.complianceService.checkCompliance(order); this.auditLogger.log({ action: 'compliance_check', orderId: order.id, result: isCompliant, }); if (!isCompliant) { return { approved: false, approver: 'Compliance System', comments: 'Order failed compliance check', timestamp: new Date(), }; } return this.forward(order); }} /** * Chain provider that uses DI container * (Example using a generic DI pattern - adapt to your framework) */interface DIContainer { resolve<T>(token: string): T;} class ChainProvider { constructor(private readonly container: DIContainer) {} getApprovalChain(): Handler<PurchaseOrder, ApprovalResult> { // Resolve handlers from DI container const compliance = this.container.resolve<Handler<PurchaseOrder, ApprovalResult>>( 'ComplianceHandler' ); const supervisor = this.container.resolve<Handler<PurchaseOrder, ApprovalResult>>( 'SupervisorHandler' ); const manager = this.container.resolve<Handler<PurchaseOrder, ApprovalResult>>( 'ManagerHandler' ); const director = this.container.resolve<Handler<PurchaseOrder, ApprovalResult>>( 'DirectorHandler' ); const ceo = this.container.resolve<Handler<PurchaseOrder, ApprovalResult>>( 'CEOHandler' ); // Assemble chain return compliance .setNext(supervisor) .setNext(manager) .setNext(director) .setNext(ceo) as Handler<PurchaseOrder, ApprovalResult>; }}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
// Example: Chain of Responsibility with NestJS-style DI import { Injectable, Inject } from '@nestjs/common'; // Injectable handlers@Injectable()class AuthHandler extends AbstractHandler<HttpRequest, HttpResponse> { constructor( @Inject('AuthService') private readonly authService: AuthService ) { super(); } handle(request: HttpRequest): HttpResponse | null { const token = request.headers['Authorization']; if (!token || !this.authService.validate(token)) { return { status: 401, body: 'Unauthorized' }; } return this.forward(request); }} @Injectable()class RateLimitHandler extends AbstractHandler<HttpRequest, HttpResponse> { constructor( @Inject('RateLimiter') private readonly rateLimiter: RateLimiter ) { super(); } handle(request: HttpRequest): HttpResponse | null { if (!this.rateLimiter.allow(request.clientId)) { return { status: 429, body: 'Too Many Requests' }; } return this.forward(request); }} // Chain factory as injectable service@Injectable()class MiddlewareChainFactory { constructor( private readonly authHandler: AuthHandler, private readonly rateLimitHandler: RateLimitHandler, private readonly loggingHandler: LoggingHandler, private readonly corsHandler: CorsHandler, ) {} createChain(): Handler<HttpRequest, HttpResponse> { return this.rateLimitHandler .setNext(this.authHandler) .setNext(this.corsHandler) .setNext(this.loggingHandler) as Handler<HttpRequest, HttpResponse>; }}When using DI, consider handler lifecycle carefully. Singleton handlers (one instance, reused) are efficient but must be stateless and thread-safe. Transient handlers (new instance per request) are safer for stateful handling but have allocation overhead. Scoped handlers (one per request scope) balance the two.
Some systems require modifying chains at runtime—adding handlers, removing handlers, or reordering based on changing conditions. This requires careful design to maintain consistency and thread safety.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
/** * Dynamic Chain Manager - supports runtime chain modification * * Use cases: * - Feature flags enabling/disabling handlers * - A/B testing different handler orderings * - Health-based handler removal (unhealthy service = skip handler) * - Time-based handler activation (business hours handlers) */class DynamicChainManager<T, R> { private handlers: Map<string, Handler<T, R>> = new Map(); private orderConfig: string[] = []; private cachedChain: Handler<T, R> | null = null; private dirty: boolean = true; /** * Registers a handler by name. */ registerHandler(name: string, handler: Handler<T, R>): void { this.handlers.set(name, handler); this.dirty = true; } /** * Unregisters a handler by name. */ unregisterHandler(name: string): void { this.handlers.delete(name); this.dirty = true; } /** * Sets the desired handler order. * Handlers not in this list are excluded from the chain. */ setOrder(order: string[]): void { this.orderConfig = [...order]; this.dirty = true; } /** * Enables a handler by adding it to the order. */ enableHandler(name: string, position: number = -1): void { if (!this.orderConfig.includes(name)) { if (position === -1) { this.orderConfig.push(name); } else { this.orderConfig.splice(position, 0, name); } this.dirty = true; } } /** * Disables a handler by removing it from the order. */ disableHandler(name: string): void { const index = this.orderConfig.indexOf(name); if (index !== -1) { this.orderConfig.splice(index, 1); this.dirty = true; } } /** * Gets the current chain, rebuilding if dirty. */ getChain(): Handler<T, R> | null { if (this.dirty || !this.cachedChain) { this.cachedChain = this.buildChain(); this.dirty = false; } return this.cachedChain; } private buildChain(): Handler<T, R> | null { // Filter to only handlers in order that are registered const orderedHandlers = this.orderConfig .filter(name => this.handlers.has(name)) .map(name => this.handlers.get(name)!); if (orderedHandlers.length === 0) { return null; } // Reset all handlers' next pointers before rebuilding orderedHandlers.forEach(h => h.setNext(null as any)); // Clear existing links // Link handlers for (let i = 0; i < orderedHandlers.length - 1; i++) { orderedHandlers[i].setNext(orderedHandlers[i + 1]); } return orderedHandlers[0]; } /** * Forces chain rebuild on next getChain() call. */ invalidate(): void { this.dirty = true; }}123456789101112131415161718192021222324252627282930313233343536373839
// Example: Feature flag-driven chain modification const manager = new DynamicChainManager<PurchaseOrder, ApprovalResult>(); // Register all available handlersmanager.registerHandler('compliance', new ComplianceHandler());manager.registerHandler('supervisor', new SupervisorHandler());manager.registerHandler('manager', new ManagerHandler());manager.registerHandler('director', new DirectorHandler());manager.registerHandler('ceo', new CEOHandler());manager.registerHandler('experimental', new ExperimentalHandler()); // Set initial order (experimental disabled)manager.setOrder(['compliance', 'supervisor', 'manager', 'director', 'ceo']); // Feature flag check at runtimeif (featureFlags.isEnabled('experimental_approval')) { manager.enableHandler('experimental', 1); // Insert after compliance} // Health check integrationhealthMonitor.on('service_unhealthy', (serviceName) => { if (serviceName === 'compliance-service') { manager.disableHandler('compliance'); // Temporarily skip alerting.warn('Compliance handler disabled due to unhealthy service'); }}); healthMonitor.on('service_healthy', (serviceName) => { if (serviceName === 'compliance-service') { manager.enableHandler('compliance', 0); // Re-enable at start }}); // Use the chainconst chain = manager.getChain();if (chain) { const result = chain.handle(order);}The dynamic chain manager shown is not thread-safe. In multi-threaded environments, chain modifications during request processing can cause inconsistencies. Use locks/mutexes around modifications, or implement immutable chain rebuilding where changes create new chain instances rather than modifying existing ones.
Sometimes you need entirely different chains for different contexts—different user types, different request categories, or different regions. A Chain Registry pattern provides clean context-to-chain mapping.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
/** * Context for chain selection */interface ApprovalContext { region: 'US' | 'EU' | 'APAC'; department: string; orderCategory: 'EQUIPMENT' | 'SERVICES' | 'TRAVEL' | 'CONTRACTOR'; priority: 'NORMAL' | 'HIGH' | 'URGENT';} /** * Chain Registry - maps contexts to appropriate chains */class ApprovalChainRegistry { private chains: Map<string, Handler<PurchaseOrder, ApprovalResult>> = new Map(); private defaultChain: Handler<PurchaseOrder, ApprovalResult>; constructor(defaultChain: Handler<PurchaseOrder, ApprovalResult>) { this.defaultChain = defaultChain; } /** * Registers a chain for a specific context key. */ register(key: string, chain: Handler<PurchaseOrder, ApprovalResult>): void { this.chains.set(key, chain); } /** * Gets the appropriate chain for a given context. * Falls back to default if no specific chain is registered. */ getChain(context: ApprovalContext): Handler<PurchaseOrder, ApprovalResult> { // Try specific key combinations from most to least specific const keys = this.generateContextKeys(context); for (const key of keys) { if (this.chains.has(key)) { return this.chains.get(key)!; } } return this.defaultChain; } private generateContextKeys(context: ApprovalContext): string[] { // Generate keys from most specific to least specific return [ `${context.region}:${context.department}:${context.orderCategory}:${context.priority}`, `${context.region}:${context.department}:${context.orderCategory}`, `${context.region}:${context.orderCategory}:${context.priority}`, `${context.region}:${context.orderCategory}`, `${context.region}:${context.priority}`, `${context.orderCategory}:${context.priority}`, context.region, context.orderCategory, context.priority, ]; }}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
// Setup: Register context-specific chains const defaultChain = ApprovalChainFactory.createStandardChain();const registry = new ApprovalChainRegistry(defaultChain); // EU has additional GDPR compliance stepconst euChain = new ApprovalChainBuilder<PurchaseOrder, ApprovalResult>() .addHandler(new GDPRComplianceHandler()) .addHandler(new SupervisorHandler()) .addHandler(new ManagerHandler()) .addHandler(new DirectorHandler()) .addHandler(new CEOHandler()) .build();registry.register('EU', euChain); // CONTRACTOR orders require legal reviewconst contractorChain = new ApprovalChainBuilder<PurchaseOrder, ApprovalResult>() .addHandler(new LegalReviewHandler()) .addHandler(new SupervisorHandler()) .addHandler(new ManagerHandler()) .addHandler(new DirectorHandler()) .addHandler(new CEOHandler()) .build();registry.register('CONTRACTOR', contractorChain); // URGENT priority bypasses lower levelsconst urgentChain = new ApprovalChainBuilder<PurchaseOrder, ApprovalResult>() .addHandler(new DirectorHandler()) .addHandler(new CEOHandler()) .build();registry.register('URGENT', urgentChain); // EU + CONTRACTOR gets combined requirementsconst euContractorChain = new ApprovalChainBuilder<PurchaseOrder, ApprovalResult>() .addHandler(new GDPRComplianceHandler()) .addHandler(new LegalReviewHandler()) .addHandler(new SupervisorHandler()) .addHandler(new ManagerHandler()) .addHandler(new DirectorHandler()) .addHandler(new CEOHandler()) .build();registry.register('EU:CONTRACTOR', euContractorChain); // Usagefunction processOrder(order: PurchaseOrder, context: ApprovalContext): ApprovalResult | null { const chain = registry.getChain(context); return chain.handle(order);} // Example usageconst context: ApprovalContext = { region: 'EU', department: 'Engineering', orderCategory: 'CONTRACTOR', priority: 'NORMAL',}; const result = processOrder(order, context);// Uses EU:CONTRACTOR chain with GDPR + Legal handlersThe key generation strategy determines how specific vs general chains are matched. The example uses a specificity-ordered approach: try the most specific key first, fall back to less specific. Alternative strategies include hash-based exact matching or rule-based selection with explicit priorities.
Chain construction is its own testable concern. Separate from testing handler logic, we should test that chains are assembled correctly, configurations produce expected chains, and edge cases are handled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
describe('ApprovalChainBuilder', () => { it('should build chain with all handlers in order', () => { // Arrange const handler1 = new MockHandler('first'); const handler2 = new MockHandler('second'); const handler3 = new MockHandler('third'); // Act const chain = new ApprovalChainBuilder<TestRequest, TestResult>() .addHandler(handler1) .addHandler(handler2) .addHandler(handler3) .build(); // Assert - verify chain structure expect(chain).toBe(handler1); expect(handler1.getNext()).toBe(handler2); expect(handler2.getNext()).toBe(handler3); expect(handler3.getNext()).toBeNull(); }); it('should conditionally include handlers', () => { // Arrange const required = new MockHandler('required'); const conditional = new MockHandler('conditional'); // Act - condition is false const chainWithout = new ApprovalChainBuilder<TestRequest, TestResult>() .addHandler(required) .addHandlerIf(false, conditional) .build(); // Assert expect(chainWithout).toBe(required); expect(required.getNext()).toBeNull(); // Act - condition is true const chainWith = new ApprovalChainBuilder<TestRequest, TestResult>() .addHandler(required) .addHandlerIf(true, conditional) .build(); // Assert expect(chainWith).toBe(required); expect(required.getNext()).toBe(conditional); }); it('should throw on empty chain', () => { const builder = new ApprovalChainBuilder<TestRequest, TestResult>(); expect(() => builder.build()).toThrow('Cannot build empty chain'); });}); describe('ApprovalChainRegistry', () => { let registry: ApprovalChainRegistry; let defaultChain: Handler<PurchaseOrder, ApprovalResult>; beforeEach(() => { defaultChain = new MockHandler('default'); registry = new ApprovalChainRegistry(defaultChain); }); it('should return specific chain for matching context', () => { const euChain = new MockHandler('eu'); registry.register('EU', euChain); const context: ApprovalContext = { region: 'EU', department: 'Any', orderCategory: 'EQUIPMENT', priority: 'NORMAL', }; expect(registry.getChain(context)).toBe(euChain); }); it('should return default chain for no matching context', () => { const context: ApprovalContext = { region: 'APAC', department: 'Any', orderCategory: 'EQUIPMENT', priority: 'NORMAL', }; expect(registry.getChain(context)).toBe(defaultChain); }); it('should prefer more specific context keys', () => { const euChain = new MockHandler('eu'); const euContractorChain = new MockHandler('eu-contractor'); registry.register('EU', euChain); registry.register('EU:CONTRACTOR', euContractorChain); const context: ApprovalContext = { region: 'EU', department: 'Any', orderCategory: 'CONTRACTOR', priority: 'NORMAL', }; expect(registry.getChain(context)).toBe(euContractorChain); });});We've explored chain construction from simple assembly through sophisticated patterns. Let's consolidate the key insights:
What's next:
With pattern fundamentals and construction techniques mastered, we're ready to explore real-world applications. The next page presents practical use cases and examples—from middleware pipelines to event systems to approval workflows—demonstrating how Chain of Responsibility solves problems across diverse domains.
You now have comprehensive knowledge of chain construction patterns. You can apply factory, builder, and DI-based construction, implement dynamic chain modification, and design context-aware chain selection. These techniques enable production-grade Chain of Responsibility implementations.