Chain of Responsibility
Passes a request along a chain of handlers until one handles it.
Chain of Responsibility Pattern: A Complete Guide with Examples in 11 Programming Languages
The Chain of Responsibility Pattern is a behavioral design pattern that lets you pass requests along a chain of handlers. Each handler decides either to process the request or to pass it to the next handler in the chain. This decouples the sender of a request from its receivers, giving more than one object a chance to handle the request without the sender needing to know which object will ultimately do so. Whether you're building middleware pipelines, event systems, approval workflows, or input validation chains, Chain of Responsibility is one of the most practical and widely deployed patterns in modern software.
In this comprehensive guide, we'll explore the Chain of Responsibility Pattern with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin — complete with language-specific best practices, common pitfalls, and when to use this powerful pattern.
Table of Contents
- What is the Chain of Responsibility Pattern?
- Why Use the Chain of Responsibility Pattern?
- Chain of Responsibility Pattern Comparison
- Chain of Responsibility Pattern Explained
- Class Diagram
- Beginner-Friendly Example
- Production-Ready Example
- Real-World Use Cases
- Language-Specific Mistakes and Anti-Patterns
- Frequently Asked Questions
- Key Takeaways
What is the Chain of Responsibility Pattern?
The Chain of Responsibility Pattern organizes handlers into a chain. A request enters the first handler; that handler either processes it and stops, processes it and passes it on, or skips processing and passes it on — depending on the variant. The request travels down the chain until a handler claims it, or it reaches the end unhandled.
Think of it like a support ticket escalation system. A support ticket first goes to Level 1 support. If they can't resolve it, it gets escalated to Level 2, then to a senior engineer, and finally to the engineering team lead. Each level either resolves the ticket or passes it up. The person who filed the ticket doesn't need to know who will ultimately handle it.
Why Use the Chain of Responsibility Pattern?
The Chain of Responsibility Pattern offers several compelling benefits:
- Decoupling: The sender doesn't know which handler will process the request — it just fires and forgets into the chain
- Single Responsibility: Each handler focuses on one specific concern, keeping logic clean and focused
- Open/Closed Principle: Add new handlers to the chain without modifying existing handlers or the client
- Dynamic Composition: Chains can be assembled, reordered, and modified at runtime
- Fallback Handling: Requests that no handler claims can be caught at the end of the chain for default processing
- Separation of Cross-Cutting Concerns: Logging, authentication, validation, and rate limiting each live in their own handler
- Testability: Each handler can be unit-tested in isolation with a mock next handler
Chain of Responsibility Pattern Comparison
Let's compare Chain of Responsibility with related behavioral patterns:
| Pattern | Request Routing | Handler Knowledge | Flexibility | Use Case |
|---|---|---|---|---|
| Chain of Responsibility | Sequential chain, may stop early | Each handler knows only next | High — chain assembled at runtime | Middleware, workflows, escalation |
| Command | Direct invocation of a command object | Invoker knows command interface | Medium | Undo/redo, queuing, macros |
| Mediator | Centralized hub dispatches to handlers | Mediator knows all components | Medium | GUI components, chat systems |
| Observer | Broadcast to all subscribers | Publisher knows subscriber interface | High | Event systems, notifications |
| Strategy | Single selected algorithm | Context knows all strategies | Medium | Swappable algorithms |
Key Distinction: In Chain of Responsibility, a request is handled by one (or few) handlers chosen dynamically at runtime by the chain itself — unlike Observer, where all subscribers receive the event.
Chain of Responsibility Pattern Explained
The Chain of Responsibility Pattern typically involves:
Core Components:
- Handler Interface: Declares a method for handling requests and a method (or property) for setting the next handler. Often provides a default implementation that forwards to the next handler.
- Abstract Handler (optional): A base class that holds the reference to the next handler and provides the default "pass to next" behavior, so concrete handlers only need to override the logic for their specific case.
- Concrete Handlers: Each implements the handling logic for a specific request type or condition. Decides to handle, pass, or both.
- Client: Assembles the chain and fires the first request into it.
Common Chain Variants:
- Pure Chain: A request is handled by exactly one handler. Once handled, the chain stops.
- Pipeline: Every handler processes the request in turn (like middleware). The request is transformed as it flows through.
- Broadcast Chain: All handlers see the request regardless of whether a previous handler claimed it.
- Tree Chain: Handlers are organized as a tree rather than a linear sequence (e.g., event bubbling in the DOM).
How the Chain is Assembled:
Chains are typically assembled in one of three ways:
- Explicit linking: Each handler holds a reference to the next, set via a
set_next()method or constructor injection. - List-based pipeline: A runner iterates over a list of handlers in order (common in middleware frameworks).
- Builder/Factory: A factory or builder constructs the chain from configuration, hiding assembly from the client.
Class Diagram
Here's the UML class diagram showing the Chain of Responsibility structure:
Beginner-Friendly Example
Let's start with a classic example: a technical support escalation system. Support tickets are first handled by Level 1 support. If they can't resolve the issue, the ticket escalates to Level 2, then to a Senior Engineer.
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Optional
# Handler interface
class SupportHandler(ABC):
def __init__(self) -> None:
self._next: Optional[SupportHandler] = None
def set_next(self, handler: SupportHandler) -> SupportHandler:
"""Set the next handler and return it for fluent chaining."""
self._next = handler
return handler
def handle(self, ticket: dict) -> Optional[str]:
"""Default: pass to next handler if present."""
if self._next:
return self._next.handle(ticket)
return None
@property
def name(self) -> str:
return self.__class__.__name__
# Concrete Handlers
class Level1Support(SupportHandler):
def handle(self, ticket: dict) -> Optional[str]:
if ticket["severity"] == "low":
return f"[{self.name}] Resolved ticket #{ticket['id']}: '{ticket['issue']}'"
print(f"[{self.name}] Cannot handle severity={ticket['severity']}, escalating...")
return super().handle(ticket)
class Level2Support(SupportHandler):
def handle(self, ticket: dict) -> Optional[str]:
if ticket["severity"] in ("low", "medium"):
return f"[{self.name}] Resolved ticket #{ticket['id']}: '{ticket['issue']}'"
print(f"[{self.name}] Cannot handle severity={ticket['severity']}, escalating...")
return super().handle(ticket)
class SeniorEngineer(SupportHandler):
def handle(self, ticket: dict) -> Optional[str]:
# Senior engineer handles everything
return f"[{self.name}] Resolved ticket #{ticket['id']}: '{ticket['issue']}'"
class UnhandledTicketLogger(SupportHandler):
"""End-of-chain fallback — catches anything that slipped through."""
def handle(self, ticket: dict) -> Optional[str]:
return f"[UnhandledLogger] ⚠ Ticket #{ticket['id']} was not handled! Logged for review."
# Client
if __name__ == "__main__":
# Build the chain
level1 = Level1Support()
level2 = Level2Support()
senior = SeniorEngineer()
fallback = UnhandledTicketLogger()
# Fluent chain assembly
level1.set_next(level2).set_next(senior).set_next(fallback)
tickets = [
{"id": 1, "severity": "low", "issue": "Can't find the settings menu"},
{"id": 2, "severity": "medium", "issue": "Email notifications not working"},
{"id": 3, "severity": "high", "issue": "Production database unreachable"},
{"id": 4, "severity": "critical", "issue": "Data corruption in user accounts"},
]
print("=== Processing Support Tickets ===\n")
for ticket in tickets:
print(f"Ticket #{ticket['id']} [{ticket['severity']}]: {ticket['issue']}")
result = level1.handle(ticket)
print(f" → {result}\n")
Production-Ready Example
Now let's tackle a realistic scenario: an HTTP middleware pipeline. Processing an incoming API request requires authentication, rate limiting, request validation, and logging — each as an independent, composable handler in a pipeline. Every handler runs in sequence (pipeline variant), and any handler can short-circuit the chain by returning an error response.
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional, Dict, Callable
from collections import defaultdict
import time
import hashlib
import json
@dataclass
class HttpRequest:
method: str
path: str
headers: Dict[str, str] = field(default_factory=dict)
body: Optional[dict] = None
user_id: Optional[int] = None # Populated by auth handler
@dataclass
class HttpResponse:
status: int
body: dict
headers: Dict[str, str] = field(default_factory=dict)
@staticmethod
def ok(body: dict) -> "HttpResponse":
return HttpResponse(200, body)
@staticmethod
def error(status: int, message: str) -> "HttpResponse":
return HttpResponse(status, {"error": message})
# Handler base
class MiddlewareHandler(ABC):
def __init__(self) -> None:
self._next: Optional[MiddlewareHandler] = None
def set_next(self, handler: MiddlewareHandler) -> MiddlewareHandler:
self._next = handler
return handler
def next(self, request: HttpRequest) -> HttpResponse:
if self._next:
return self._next.handle(request)
return HttpResponse.error(500, "No handler at end of chain")
@abstractmethod
def handle(self, request: HttpRequest) -> HttpResponse:
pass
# --- Handler 1: Request Logger ---
class RequestLoggerMiddleware(MiddlewareHandler):
def handle(self, request: HttpRequest) -> HttpResponse:
start = time.monotonic()
ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
print(f"[Logger] {ts} → {request.method} {request.path}")
response = self.next(request)
elapsed = (time.monotonic() - start) * 1000
print(f"[Logger] {ts} ← {response.status} ({elapsed:.1f}ms)")
return response
# --- Handler 2: Authentication ---
VALID_TOKENS: Dict[str, int] = {
"token-admin-001": 1,
"token-user-002": 2,
"token-user-003": 3,
}
class AuthMiddleware(MiddlewareHandler):
PUBLIC_PATHS = {"/health", "/login"}
def handle(self, request: HttpRequest) -> HttpResponse:
if request.path in self.PUBLIC_PATHS:
return self.next(request)
token = request.headers.get("Authorization", "").removeprefix("Bearer ").strip()
user_id = VALID_TOKENS.get(token)
if not user_id:
print(f"[Auth] Rejected — invalid or missing token")
return HttpResponse.error(401, "Unauthorized")
request.user_id = user_id
print(f"[Auth] Authenticated user_id={user_id}")
return self.next(request)
# --- Handler 3: Rate Limiter ---
class RateLimiterMiddleware(MiddlewareHandler):
def __init__(self, max_requests: int = 5, window_seconds: float = 10.0) -> None:
super().__init__()
self._max = max_requests
self._window = window_seconds
self._counts: Dict[str, list] = defaultdict(list)
def _get_key(self, request: HttpRequest) -> str:
return f"user:{request.user_id}" if request.user_id else f"ip:anon"
def handle(self, request: HttpRequest) -> HttpResponse:
key = self._get_key(request)
now = time.monotonic()
window_start = now - self._window
# Keep only timestamps within the current window
self._counts[key] = [t for t in self._counts[key] if t > window_start]
if len(self._counts[key]) >= self._max:
print(f"[RateLimiter] Rate limit exceeded for {key}")
return HttpResponse.error(429, "Too Many Requests")
self._counts[key].append(now)
remaining = self._max - len(self._counts[key])
print(f"[RateLimiter] {key} — {remaining} requests remaining in window")
return self.next(request)
# --- Handler 4: Input Validator ---
REQUIRED_FIELDS: Dict[str, Dict[str, list]] = {
"POST /users": {"required": ["name", "email"]},
"POST /payments": {"required": ["amount", "currency", "recipient_id"]},
}
class ValidationMiddleware(MiddlewareHandler):
def handle(self, request: HttpRequest) -> HttpResponse:
route_key = f"{request.method} {request.path}"
rules = REQUIRED_FIELDS.get(route_key)
if rules is None:
return self.next(request) # No rules — pass through
if not request.body:
return HttpResponse.error(400, "Request body is required")
missing = [f for f in rules["required"] if f not in request.body]
if missing:
print(f"[Validator] Missing fields: {missing}")
return HttpResponse.error(400, f"Missing required fields: {missing}")
print(f"[Validator] Validation passed for {route_key}")
return self.next(request)
# --- Final Handler: Router ---
class RouterHandler(MiddlewareHandler):
def __init__(self) -> None:
super().__init__()
self._routes: Dict[str, Callable[[HttpRequest], HttpResponse]] = {}
def register(self, method: str, path: str,
handler: Callable[[HttpRequest], HttpResponse]) -> None:
self._routes[f"{method} {path}"] = handler
def handle(self, request: HttpRequest) -> HttpResponse:
key = f"{request.method} {request.path}"
handler = self._routes.get(key)
if handler:
return handler(request)
return HttpResponse.error(404, f"Route '{key}' not found")
# --- Application assembly ---
def build_pipeline() -> MiddlewareHandler:
router = RouterHandler()
router.register("GET", "/health", lambda r: HttpResponse.ok({"status": "healthy"}))
router.register("GET", "/users", lambda r: HttpResponse.ok({"users": [{"id": r.user_id}]}))
router.register("POST", "/users", lambda r: HttpResponse.ok({"created": r.body}))
logger = RequestLoggerMiddleware()
auth = AuthMiddleware()
limiter = RateLimiterMiddleware(max_requests=5, window_seconds=10.0)
validator = ValidationMiddleware()
logger.set_next(auth).set_next(limiter).set_next(validator).set_next(router)
return logger
# Client
if __name__ == "__main__":
pipeline = build_pipeline()
def send(method, path, headers=None, body=None):
req = HttpRequest(method=method, path=path,
headers=headers or {}, body=body)
resp = pipeline.handle(req)
print(f" Response: {resp.status} {json.dumps(resp.body)}\n")
print("=== 1. Public health check (no auth needed) ===")
send("GET", "/health")
print("=== 2. Authenticated GET /users ===")
send("GET", "/users", headers={"Authorization": "Bearer token-user-002"})
print("=== 3. Invalid token ===")
send("GET", "/users", headers={"Authorization": "Bearer bad-token"})
print("=== 4. POST /users with missing fields ===")
send("POST", "/users",
headers={"Authorization": "Bearer token-admin-001"},
body={"name": "Alice"}) # Missing 'email'
print("=== 5. POST /users with valid body ===")
send("POST", "/users",
headers={"Authorization": "Bearer token-admin-001"},
body={"name": "Alice", "email": "alice@example.com"})
print("=== 6. Rate limiting — 6 rapid requests ===")
for i in range(6):
send("GET", "/users", headers={"Authorization": "Bearer token-user-003"})
Real-World Use Cases
1. Web Framework Middleware
Every major web framework — Express (Node.js), Django (Python), ASP.NET Core (C#), Laravel (PHP) — is built on Chain of Responsibility. Each middleware in the pipeline is a handler that can process, short-circuit, or pass the request to the next handler.
# Django middleware — each class is a handler in the chain
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", # Handler 1
"django.middleware.gzip.GZipMiddleware", # Handler 2
"django.contrib.sessions.middleware.SessionMiddleware",# Handler 3
"django.contrib.auth.middleware.AuthenticationMiddleware", # Handler 4
# ...
]
2. Event Bubbling in the DOM
When a user clicks a button, the event bubbles up through the DOM — from the button, to its parent div, to the body, to the document. Each element is a handler in the chain, and calling event.stopPropagation() short-circuits it.
3. Approval / Workflow Systems
Purchase approval workflows are a textbook Chain of Responsibility use case: a team lead approves purchases up to $1,000, a department manager up to $10,000, a VP up to $100,000, and the CEO above that.
class TeamLeadApprover(ApprovalHandler):
LIMIT = 1_000
def handle(self, request: PurchaseRequest):
if request.amount <= self.LIMIT:
return f"[TeamLead] Approved ${request.amount}"
return super().handle(request)
4. Logging Frameworks
Log4j, Python's logging module, and other logging frameworks use Chain of Responsibility for log handlers: a message at DEBUG level might pass through a console handler, a file handler, and a remote handler in sequence.
5. Input Validation Pipelines
Form or API input validation can be decomposed into a chain: sanitize whitespace → check required fields → validate email format → check uniqueness in DB → reject or accept. Each validator is an independent, testable handler.
6. GUI Event Systems
Keyboard events in GUI frameworks pass through a chain of components (focused widget → parent container → window → application). The first component that can handle the key press consumes it; others pass it on.
Language-Specific Mistakes and Anti-Patterns
❌ Mistake #1: Not Returning the Next Handler From set_next()
# BAD: setNext doesn't return the handler — fluent chaining is impossible
class AbstractHandler:
def set_next(self, handler):
self._next = handler
# Returns None implicitly!
# Forces verbose chaining:
l1.set_next(l2)
l2.set_next(l3)
l3.set_next(l4)
# GOOD: Return the handler so the chain can be built fluently
class AbstractHandler:
def set_next(self, handler):
self._next = handler
return handler # Enables: l1.set_next(l2).set_next(l3).set_next(l4)
❌ Mistake #2: Infinite Loop — Handler Calls Itself
# BAD: Forgetting to call super().handle() or self._next.handle() — silently drops the request
class MyHandler(AbstractHandler):
def handle(self, request):
if self.can_handle(request):
return self.process(request)
# Falls off the end — returns None, chain is silently broken!
# GOOD: Always explicitly pass to next when not handling
class MyHandler(AbstractHandler):
def handle(self, request):
if self.can_handle(request):
return self.process(request)
return super().handle(request) # Forward to next
❌ Mistake #3: Mutable Request State Causing Side Effects
# BAD: Handlers mutate the shared request object in ways later handlers don't expect
class NormalizationHandler(AbstractHandler):
def handle(self, request):
request.path = request.path.lower() # Mutates in-place!
request.headers = {} # Clears all headers!
return super().handle(request)
# Later handlers see a broken request without any indication why.
# GOOD: Either document mutations clearly, or pass a copy
class NormalizationHandler(AbstractHandler):
def handle(self, request):
import copy
normalized = copy.replace(request, path=request.path.lower())
return super().handle(normalized) # Pass a modified copy
Frequently Asked Questions
Q1: What is the difference between Chain of Responsibility and a simple if-else chain?
A long if-else if or switch block hard-codes every condition in one place. Adding a new case means editing that block — a violation of the Open/Closed Principle. Chain of Responsibility externalizes each condition into its own class/object. New cases are added by creating a new handler and inserting it into the chain — existing handlers are untouched. The chain is also assembled at runtime, making it dynamic and reconfigurable.
Q2: Should a handled request always stop at the handler that claims it?
Not necessarily — it depends on the variant you're implementing:
- Pure chain (stop on first match): The first handler that can process the request does so and the chain ends. Good for: escalation systems, routing, command dispatch.
- Pipeline (all handlers run): Every handler runs in sequence, each transforming or enriching the request. Good for: middleware, validation pipelines, data processing.
- Conditional pipeline: A handler can stop the chain early (short-circuit) by not calling the next handler. Good for: HTTP middleware where an auth failure should prevent further processing.
Q3: What happens if no handler in the chain handles a request?
The request reaches the end of the chain unhandled and typically returns null, None, or an empty Optional. This is often a silent failure. Best practices:
- Always add a terminal handler (fallback) at the end that handles everything — either with a default response or with an explicit log/alert that the request went unhandled.
- Return a meaningful default rather than
nullwhere possible. - Log unhandled requests so they don't disappear silently.
Q4: How is Chain of Responsibility different from the Strategy Pattern?
In Strategy, the client explicitly selects one algorithm from several options before executing. There is no chain — just a single selected strategy.
In Chain of Responsibility, the client fires a request at the first handler and doesn't know which handler (if any) will ultimately process it. The chain itself decides. Multiple handlers may even partially handle the same request in pipeline mode.
Q5: What is the relationship between Chain of Responsibility and middleware?
They are architecturally identical. Web middleware (Express, Django, ASP.NET Core) is Chain of Responsibility applied to HTTP requests. Each middleware component is a handler. The next() function call is the "pass to next handler" operation. Short-circuiting a middleware by not calling next() is exactly "handler claims the request and stops the chain."
Q6: How do I handle errors in the chain?
There are three common approaches:
- Translate to error responses (pipeline): Handlers catch exceptions and return structured error responses (e.g., HTTP 500) rather than letting them propagate.
- Propagate naturally: Exceptions bubble up through the chain to the top-level caller, which handles them centrally.
- Error handler at the end: Add an error-catching handler at the end of the chain (like Express's
app.use((err, req, res, next) => {...})error middleware).
Q7: Can I reuse the same handler instance in multiple chains?
Only if the handler is stateless. If a handler holds mutable state (like a rate limiter that counts requests), sharing it across chains means those chains share state — which may be intentional (shared rate limit bucket) or a bug (contaminated chain state). Stateless handlers (pure logic only) are freely sharable.
Q8: How do I test a chain handler in isolation?
Inject a mock for the next handler and verify that:
- When the handler processes the request, it returns the correct result and does NOT call next.
- When the handler doesn't process the request, it calls next exactly once with the correct request.
from unittest.mock import Mock
def test_level1_handles_low_severity():
mock_next = Mock()
handler = Level1Support()
handler.set_next(mock_next)
result = handler.handle({"id": 1, "severity": "low", "issue": "minor"})
assert "Level1" in result
mock_next.handle.assert_not_called() # Did NOT pass to next
def test_level1_escalates_high_severity():
mock_next = Mock(return_value="[Level2] Resolved")
handler = Level1Support()
handler.set_next(mock_next)
result = handler.handle({"id": 2, "severity": "high", "issue": "critical"})
mock_next.handle.assert_called_once() # DID pass to next
assert result == "[Level2] Resolved"
Q9: When should I use a list-based pipeline vs. linked handler objects?
Linked handler objects (set_next() approach):
- Best when the chain is relatively static and set up at startup
- Handlers are more self-contained — each knows how to forward
- Easier to visualize as a linked list of objects
List-based pipeline (iterate over a list of handlers):
- Best when the chain is dynamic (handlers added/removed at runtime)
- Easier to reorder, inspect, and debug the chain
- Common in frameworks (Django MIDDLEWARE, ASP.NET pipeline)
# List-based pipeline runner
class Pipeline:
def __init__(self, handlers: list):
self._handlers = handlers
def run(self, request):
for handler in self._handlers:
result = handler.process(request)
if result.stop: # Handler signals "I handled it, stop here"
return result
return unhandled_response()
Q10: What is the best way to assemble a chain?
Use a dedicated factory function or builder at the application's composition root. Never assemble chains inside handlers themselves (violates SRP) or scatter chain assembly across multiple locations.
# Clean composition root pattern
def create_api_pipeline(config: AppConfig) -> MiddlewareHandler:
router = build_router(config)
validator = ValidationMiddleware(config.validation_rules)
limiter = RateLimiterMiddleware(config.rate_limit)
auth = AuthMiddleware(config.auth_secret)
logger = RequestLoggerMiddleware(config.log_level)
logger.set_next(auth).set_next(limiter).set_next(validator).set_next(router)
return logger
Key Takeaways
🎯 Core Concept: The Chain of Responsibility Pattern passes a request along a chain of handlers. Each handler either handles the request, passes it forward, or both. The sender is fully decoupled from the receiver — it only knows about the first handler in the chain.
🔑 Key Benefits:
- Decoupling: Sender and receiver are completely independent; the chain wires them together
- Single Responsibility: Each handler is focused on one concern — auth, validation, logging, or routing
- Open/Closed Principle: New handlers are added by inserting them into the chain; existing handlers never change
- Dynamic composition: Chains are assembled, reordered, or reconfigured at runtime without changing business logic
- Testability: Every handler is independently testable with a mock next handler
🌐 Language-Specific Highlights:
- Python: Return
handlerfromset_next()for fluent chaining; always callsuper().handle()to avoid silently dropping requests; usecopy.replace()when mutating request state - TypeScript: Use generics
Handler<TRequest, TResponse>to preserve type safety; make the entire chainasyncif any handler is async - Java: Use
Optional<T>as the return type to make "unhandled" explicit;computeIfAbsentfor thread-safe state; always terminate the chain with a fallback handler - JavaScript: Guard against
nullnext handler with optional chaining (?.); always pass handler objects, not raw methods, to avoidthisbinding issues - C#: Thread
CancellationTokenthrough all async handlers; translate exceptions to structured error responses rather than swallowing them - PHP: Define an interface for handlers — never rely on duck typing for chain correctness; avoid
instanceofchecks inside handlers - Go: Always use pointer receivers for handlers with state; define a
Handle()method on every concrete handler even when embeddingBaseHandler - Rust: Build chains from the inside out using nested
Box<dyn Trait>; useOption<T>(notResult<T, E>) for "not handled" semantics; avoidRc<RefCell<>>for handler chains - Dart: Use
?.for null-safe next handler calls; useimplements, notextends, for concrete handlers - Swift: Handlers must be
class(notstruct) to avoid value-copy issues withsetNext; useweakfor any back-references to prevent retain cycles - Kotlin: Use
bydelegation or functional composition to reduce boilerplate; never forgetoverrideor the handler logic is silently bypassed; uselazyinitialization for heavyweight handler resources
📋 When to Use Chain of Responsibility:
- More than one object may handle a request and the handler isn't known upfront
- You want to issue a request without specifying the receiver explicitly
- The set of handlers or their order needs to change at runtime
- You're implementing cross-cutting concerns (auth, logging, caching, rate limiting, validation) that should be composable and independent
- You're building a middleware system, event pipeline, approval workflow, or escalation system
⚠️ When NOT to Use Chain of Responsibility:
- There is always exactly one handler — use a simple method call or Strategy instead
- The chain is so long and static that a simple
if-elseor switch is clearer and faster - Every request must be guaranteed to be handled — a pure chain that can "miss" all handlers is risky without a terminal fallback
- Performance is critical and the chain adds unacceptable overhead for extremely hot paths
🛠️ Best Practices Across Languages:
- Return
handlerfromset_next(): Enables fluent chain assembly without verbose variable reuse - Always add a terminal fallback handler: Silence is the hardest bug to debug — make unhandled requests loud
- Assemble chains at the composition root: Build chains in a factory/builder, never inside handlers themselves
- Keep handlers stateless when possible: Stateless handlers are trivially thread-safe, testable, and reusable across chains
- Use a common base class for the "pass to next" default: Don't repeat forwarding boilerplate in every handler
- Propagate errors — don't swallow them: A handler should translate exceptions to structured responses or re-throw, never silently return
null - Test each handler in isolation: Mock the next handler to verify handle/pass-through behavior independently
- Document the chain order: The order of handlers in a chain is business logic — document it clearly and consider making it configurable
💡 Common Pitfalls:
- Silent drops: Handler doesn't call next and doesn't handle — request vanishes with no result or error
- No terminal handler: Chains that can produce
nullwhen they "fall off the end" make callers defensive everywhere - State mutation bugs: Handlers that mutate shared request objects cause subtle bugs for downstream handlers
- Value-type handlers: Using structs (Swift, Go) for handlers that hold mutable state causes copy-assignment bugs
- Async inconsistency: One async handler in a sync chain causes unresolved Promises/Tasks to propagate instead of values
- Chain built inside handlers: Couples handler construction to chain topology — kills testability and reusability
🎁 Advanced Techniques:
- Functional pipeline: In languages with first-class functions, compose handlers as
(Request) -> Responsefunctions chained viaandThenorcompose - List-based pipeline runner: Store handlers in a
List<Handler>and iterate — easier to add/remove/reorder dynamically - Interceptor pattern: A variant where handlers wrap the entire invocation (before + after), enabling timing, transactions, and retry logic
- Async pipeline with middleware context: Pass a mutable
Contextobject alongside the request so handlers can share enriched data (user identity, tracing IDs) without mutating the request - Chain introspection: Give each handler a
nameproperty so chains can be logged, visualized, or debugged asLogger → Auth → RateLimiter → Validator → Router
Chain of Responsibility is one of the patterns you'll use every day without naming it. The moment you add a middleware to an Express app, write a @Transactional annotation, or implement an event bubbling system, you're using it. Understanding it explicitly lets you design your own composable pipelines with the same clean structure that makes modern frameworks so powerful.
Want to dive deeper into design patterns? Check out our comprehensive guides on:
- Proxy Pattern
- Facade Pattern
- Decorator Pattern
- Command Pattern
- Observer Pattern Each guide includes examples in all 11 programming languages with language-specific best practices.