Chain of Responsibility Pattern āļø
Definition: The Chain of Responsibility pattern passes requests along a chain of handlers. Upon receiving a request, each handler decides either to process the request or to pass it to the next handler in the chain.
šÆ 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 the chain until an object handles it.
š¤ Problemā
You want to process a request through a series of handlers without coupling the sender to specific handlers. Consider scenarios like:
- Request Processing: HTTP request filters, middleware
- Event Handling: GUI event propagation
- Approval Workflow: Document approval chain
- Error Handling: Different error handlers for different error types
- Validation: Multi-step validation process
The sender shouldn't know which handler will process the request or the order of handlers.
š” Solutionā
The Chain of Responsibility pattern suggests linking handlers into a chain. Each handler has a field for storing a reference to the next handler. When a handler receives a request, it can either process it or pass it along to the next handler.
šļø Structureā
Handler (abstract)
āāā +nextHandler: Handler
āāā +setNext(handler: Handler): Handler
āāā +handle(request): void
ConcreteHandler1 extends Handler
āāā +handle(request): void ā process or super.handle(request)
ConcreteHandler2 extends Handler
āāā +handle(request): void ā process or super.handle(request)
Client ā handler1.setNext(handler2).setNext(handler3)
ā handler1.handle(request)
š» Simple Exampleā
Support Ticket Systemā
// Abstract Handler
class SupportHandler {
constructor() {
this.nextHandler = null;
}
setNext(handler) {
this.nextHandler = handler;
return handler; // Return handler for method chaining
}
handle(ticket) {
if (this.canHandle(ticket)) {
return this.process(ticket);
} else if (this.nextHandler) {
console.log(`š ${this.constructor.name}: Passing to next handler`);
return this.nextHandler.handle(ticket);
} else {
console.log(
`ā No handler available for ticket: ${ticket.type} (Priority: ${ticket.priority})`,
);
return null;
}
}
canHandle(ticket) {
throw new Error('canHandle() method must be implemented');
}
process(ticket) {
throw new Error('process() method must be implemented');
}
}
// Concrete Handler - Level 1 Support
class Level1Support extends SupportHandler {
canHandle(ticket) {
return ticket.type === 'basic' && ticket.priority <= 2;
}
process(ticket) {
console.log(`šØāš» Level 1 Support: Handling ticket #${ticket.id}`);
console.log(` Type: ${ticket.type}`);
console.log(` Priority: ${ticket.priority}`);
console.log(` Issue: ${ticket.description}`);
console.log(` ā
Resolved with standard procedure`);
return {
handledBy: 'Level 1 Support',
resolution: 'Standard procedure applied',
ticketId: ticket.id,
resolvedAt: new Date(),
};
}
}
// Concrete Handler - Level 2 Support
class Level2Support extends SupportHandler {
canHandle(ticket) {
return (
(ticket.type === 'technical' && ticket.priority <= 3) ||
(ticket.type === 'basic' && ticket.priority <= 4)
);
}
process(ticket) {
console.log(`š§ Level 2 Support: Handling ticket #${ticket.id}`);
console.log(` Type: ${ticket.type}`);
console.log(` Priority: ${ticket.priority}`);
console.log(` Issue: ${ticket.description}`);
console.log(` ā
Resolved with technical expertise`);
return {
handledBy: 'Level 2 Support',
resolution: 'Technical solution provided',
ticketId: ticket.id,
resolvedAt: new Date(),
};
}
}
// Concrete Handler - Level 3 Support (Senior/Expert)
class Level3Support extends SupportHandler {
canHandle(ticket) {
return ticket.type === 'critical' || ticket.priority >= 4;
}
process(ticket) {
console.log(`šØ Level 3 Support: Handling ticket #${ticket.id}`);
console.log(` Type: ${ticket.type}`);
console.log(` Priority: ${ticket.priority}`);
console.log(` Issue: ${ticket.description}`);
console.log(` ā
Resolved by senior expert`);
return {
handledBy: 'Level 3 Support',
resolution: 'Expert-level solution provided',
ticketId: ticket.id,
resolvedAt: new Date(),
};
}
}
// Ticket class
class SupportTicket {
constructor(id, type, priority, description) {
this.id = id;
this.type = type; // 'basic', 'technical', 'critical'
this.priority = priority; // 1-5 (1=lowest, 5=highest)
this.description = description;
this.createdAt = new Date();
}
}
// Usage
console.log('=== Support Ticket Chain Demo ===\n');
console.log('Setting up support chain:');
console.log('-'.repeat(25));
// Create handlers
const level1 = new Level1Support();
const level2 = new Level2Support();
const level3 = new Level3Support();
// Set up chain: Level1 ā Level2 ā Level3
level1.setNext(level2).setNext(level3);
console.log('ā
Support chain established: Level1 ā Level2 ā Level3\n');
console.log('Processing various tickets:');
console.log('-'.repeat(30));
// Test different types of tickets
const tickets = [
new SupportTicket(1001, 'basic', 1, 'Password reset request'),
new SupportTicket(1002, 'technical', 2, 'Software installation issue'),
new SupportTicket(1003, 'basic', 3, 'Account access problem'),
new SupportTicket(1004, 'critical', 5, 'Server outage - production down'),
new SupportTicket(1005, 'technical', 4, 'Database performance issues'),
new SupportTicket(1006, 'unknown', 1, 'Unhandled ticket type'),
];
tickets.forEach((ticket, index) => {
console.log(`\n${index + 1}. Processing Ticket #${ticket.id}:`);
console.log('-'.repeat(35));
const result = level1.handle(ticket);
if (result) {
console.log(`š Resolution Summary: ${result.resolution} by ${result.handledBy}`);
}
});
š Real-World Exampleā
HTTP Request Middleware Chainā
// Abstract Middleware Handler
class Middleware {
constructor() {
this.next = null;
}
setNext(middleware) {
this.next = middleware;
return middleware;
}
handle(request, response) {
if (this.process(request, response)) {
if (this.next) {
return this.next.handle(request, response);
}
return true; // Request processed successfully
}
return false; // Request rejected
}
process(request, response) {
throw new Error('process() method must be implemented');
}
}
// Concrete Middleware - Authentication
class AuthenticationMiddleware extends Middleware {
process(request, response) {
console.log(`š AuthenticationMiddleware: Checking authentication`);
const authHeader = request.headers['authorization'];
if (!authHeader) {
console.log(`ā Authentication failed: No authorization header`);
response.status = 401;
response.body = { error: 'Authentication required' };
return false;
}
// Simulate token validation
const token = authHeader.replace('Bearer ', '');
if (token === 'invalid_token') {
console.log(`ā Authentication failed: Invalid token`);
response.status = 401;
response.body = { error: 'Invalid token' };
return false;
}
// Add user info to request
request.user = { id: 123, username: 'john_doe', role: 'user' };
console.log(`ā
Authentication successful: User ${request.user.username}`);
return true;
}
}
// Concrete Middleware - Authorization
class AuthorizationMiddleware extends Middleware {
constructor(requiredRole = 'user') {
super();
this.requiredRole = requiredRole;
}
process(request, response) {
console.log(`š”ļø AuthorizationMiddleware: Checking authorization`);
if (!request.user) {
console.log(`ā Authorization failed: No user information`);
response.status = 401;
response.body = { error: 'User not authenticated' };
return false;
}
const userRole = request.user.role;
const roleHierarchy = { user: 1, admin: 2, superadmin: 3 };
if (roleHierarchy[userRole] < roleHierarchy[this.requiredRole]) {
console.log(
`ā Authorization failed: Insufficient permissions (has: ${userRole}, needs: ${this.requiredRole})`,
);
response.status = 403;
response.body = { error: 'Insufficient permissions' };
return false;
}
console.log(`ā
Authorization successful: User has ${userRole} role`);
return true;
}
}
// Concrete Middleware - Rate Limiting
class RateLimitMiddleware extends Middleware {
constructor(maxRequests = 10, windowMinutes = 1) {
super();
this.maxRequests = maxRequests;
this.windowMs = windowMinutes * 60 * 1000;
this.requests = new Map(); // userId -> { count, resetTime }
}
process(request, response) {
console.log(`ā±ļø RateLimitMiddleware: Checking rate limits`);
const userId = request.user?.id || request.ip;
const now = Date.now();
if (!this.requests.has(userId)) {
this.requests.set(userId, { count: 0, resetTime: now + this.windowMs });
}
const userRequests = this.requests.get(userId);
// Reset window if expired
if (now > userRequests.resetTime) {
userRequests.count = 0;
userRequests.resetTime = now + this.windowMs;
}
userRequests.count++;
if (userRequests.count > this.maxRequests) {
const resetIn = Math.ceil((userRequests.resetTime - now) / 1000);
console.log(
`ā Rate limit exceeded: ${userRequests.count}/${this.maxRequests} (resets in ${resetIn}s)`,
);
response.status = 429;
response.body = {
error: 'Rate limit exceeded',
resetIn: resetIn,
};
return false;
}
console.log(`ā
Rate limit OK: ${userRequests.count}/${this.maxRequests} requests`);
return true;
}
}
// Concrete Middleware - Request Validation
class ValidationMiddleware extends Middleware {
constructor(requiredFields = []) {
super();
this.requiredFields = requiredFields;
}
process(request, response) {
console.log(`āļø ValidationMiddleware: Validating request`);
const missingFields = [];
// Check required fields in body
this.requiredFields.forEach((field) => {
if (!request.body || !request.body[field]) {
missingFields.push(field);
}
});
if (missingFields.length > 0) {
console.log(`ā Validation failed: Missing fields: ${missingFields.join(', ')}`);
response.status = 400;
response.body = {
error: 'Validation failed',
missingFields: missingFields,
};
return false;
}
console.log(`ā
Validation successful: All required fields present`);
return true;
}
}
// Concrete Middleware - Request Logger
class LoggerMiddleware extends Middleware {
process(request, response) {
const timestamp = new Date().toISOString();
console.log(`š LoggerMiddleware: ${timestamp} ${request.method} ${request.path}`);
console.log(` User-Agent: ${request.headers['user-agent'] || 'Unknown'}`);
console.log(` IP: ${request.ip}`);
// Always continue to next middleware
return true;
}
}
// HTTP Request/Response classes for demo
class HTTPRequest {
constructor(method, path, headers = {}, body = null) {
this.method = method;
this.path = path;
this.headers = headers;
this.body = body;
this.ip = '192.168.1.1';
this.user = null; // Will be set by authentication middleware
}
}
class HTTPResponse {
constructor() {
this.status = 200;
this.body = null;
this.headers = {};
}
}
// Usage
console.log('\n=== HTTP Middleware Chain Demo ===\n');
console.log('Setting up middleware chain:');
console.log('-'.repeat(30));
// Create middleware components
const logger = new LoggerMiddleware();
const auth = new AuthenticationMiddleware();
const authorization = new AuthorizationMiddleware('user');
const rateLimit = new RateLimitMiddleware(3, 1); // 3 requests per minute
const validation = new ValidationMiddleware(['title', 'content']);
// Set up middleware chain
logger.setNext(auth).setNext(authorization).setNext(rateLimit).setNext(validation);
console.log('ā
Middleware chain: Logger ā Auth ā Authorization ā RateLimit ā Validation\n');
console.log('Processing different requests:');
console.log('-'.repeat(35));
// Test requests
const testRequests = [
{
name: 'Valid authenticated request',
request: new HTTPRequest(
'POST',
'/api/posts',
{
authorization: 'Bearer valid_token',
'user-agent': 'MyApp/1.0',
},
{ title: 'My Post', content: 'Post content here' },
),
},
{
name: 'Request without authentication',
request: new HTTPRequest(
'POST',
'/api/posts',
{
'user-agent': 'MyApp/1.0',
},
{ title: 'My Post', content: 'Post content here' },
),
},
{
name: 'Request with invalid token',
request: new HTTPRequest(
'POST',
'/api/posts',
{
authorization: 'Bearer invalid_token',
'user-agent': 'MyApp/1.0',
},
{ title: 'My Post', content: 'Post content here' },
),
},
{
name: 'Request with missing validation fields',
request: new HTTPRequest(
'POST',
'/api/posts',
{
authorization: 'Bearer valid_token',
'user-agent': 'MyApp/1.0',
},
{ title: 'My Post' },
), // Missing 'content' field
},
];
testRequests.forEach((test, index) => {
console.log(`\n${index + 1}. ${test.name}:`);
console.log('-'.repeat(40));
const response = new HTTPResponse();
const success = logger.handle(test.request, response);
console.log(`\nš Result: ${success ? 'SUCCESS' : 'FAILED'}`);
if (response.status !== 200) {
console.log(` Status: ${response.status}`);
console.log(` Error:`, response.body);
}
});
// Test rate limiting
console.log(`\n${'='.repeat(50)}\n`);
console.log('Testing rate limiting with multiple requests:');
console.log('-'.repeat(45));
for (let i = 1; i <= 5; i++) {
console.log(`\nRequest ${i}:`);
console.log('-'.repeat(12));
const request = new HTTPRequest(
'POST',
'/api/posts',
{
authorization: 'Bearer valid_token',
'user-agent': 'MyApp/1.0',
},
{ title: `Post ${i}`, content: `Content for post ${i}` },
);
const response = new HTTPResponse();
const success = logger.handle(request, response);
console.log(`š Result: ${success ? 'SUCCESS' : 'FAILED'}`);
if (response.status !== 200) {
console.log(` Status: ${response.status}`);
console.log(` Error:`, response.body);
}
}
ā Prosā
- Decoupling: Decouples sender from receivers
- Flexibility: Can add/remove handlers dynamically
- Single Responsibility: Each handler has one responsibility
- Runtime Configuration: Chain can be configured at runtime
- Request Processing: Multiple handlers can process the same request
ā Consā
- Performance: Request might traverse the entire chain
- Debugging: Can be difficult to debug the chain execution
- No Guarantee: No guarantee that request will be handled
- Chain Management: Managing the chain can become complex
šÆ When to Useā
- Multiple Processors: Multiple objects can handle a request
- Dynamic Handler Set: The set of handlers should be specified dynamically
- Unknown Recipients: You don't want to specify receivers explicitly
- Request Filtering: Need to filter requests through multiple criteria
- Event Handling: GUI events that bubble up through components
š Chain Variationsā
1. Pure Chain of Responsibilityā
- Only one handler processes the request
- Request stops after first successful handling
2. Chain with Fallback (shown in examples)ā
- Handlers can reject and pass to next handler
- Default handler at the end
3. Processing Chainā
- All handlers in the chain process the request
- Each handler modifies the request
4. Conditional Chainā
class ConditionalHandler extends Handler {
constructor(condition) {
super();
this.condition = condition;
}
handle(request) {
if (this.condition(request)) {
return this.process(request);
} else if (this.nextHandler) {
return this.nextHandler.handle(request);
}
return null;
}
}
š Related Patternsā
- Decorator: Both use object composition, but Chain of Responsibility passes requests while Decorator adds behavior
- Command: Both can implement undo/redo, but Command encapsulates requests while Chain of Responsibility processes them
- Composite: Chain uses composition to build handlers, similar to Composite's tree structure
- Template Method: Handlers might use Template Method to define processing steps