Skip to main content

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;
}
}
  • 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

šŸ“š Further Reading​