Chain of Responsibility: Interview Deep Dive
Common Interview Questions
Question 1: How do you prevent infinite loops in the chain?
Answer: Infinite loops occur if the chain forms a cycle, such as HandlerA points to HandlerB, which points back to HandlerA. To prevent this, follow these approaches:
First, ensure that setNext() only allows setting the next handler once or validates that the handler being added is not already in the chain. Use a visited set to detect cycles during chain construction.
Second, design handlers to always move forward. A handler should either process the request and stop, or forward it to the next handler without looping back. Never allow a handler to call a previous handler in the chain.
Third, add a maximum chain length limit or traversal counter. If a request passes through more than N handlers (for example, 50), throw an exception assuming a configuration error exists.
Question 2: Should a handler always forward to the next handler, or can it stop the chain?
Answer: It depends on the use case. There are two valid approaches:
Stop on First Match: Used in authentication chains or validation pipelines. Once a handler successfully processes the request, the chain stops. For example, if Token Authentication succeeds, do not try Basic Authentication. Implement this by simply not calling nextHandler.handle() after successful processing.
Process and Continue: Used in logging or middleware pipelines. Each handler does its work and then forwards the request regardless. For example, a logging handler records the request and always passes it forward. Implement this by calling nextHandler.handle() after processing, not instead of processing.
The key is consistency. All handlers in a chain should follow the same forwarding convention, otherwise behavior becomes unpredictable.
Question 3: How do you handle requests that no handler processes?
Answer: This is a critical edge case. Use one of these strategies:
Default Handler: Place a catch-all handler at the end of the chain that always processes requests. For example, in an approval workflow, the final handler could be an administrator approval that accepts any request amount. This guarantees every request gets handled.
Explicit Validation: Have the client check if the request was processed after calling the chain. The WithdrawalRequest in the ATM example could have an isFullyProcessed() method that returns false if any amount remains. The client then throws an exception or displays an error message.
Null Object Pattern: Instead of allowing nextHandler to be null, use a NullHandler that does nothing. This eliminates null checks throughout the chain, but you still need to track if actual processing occurred.
Machine Coding Considerations
Chain Builder Pattern: In machine coding rounds, you may be asked to implement a fluent interface for building chains. This improves readability:
ChainBuilder.start()
.addHandler(new AuthenticationHandler())
.addHandler(new AuthorizationHandler())
.addHandler(new RateLimitHandler())
.build()The builder internally calls setNext() on each handler, maintaining the chain structure. It can also validate the chain (for example, ensure no null handlers, detect cycles).
Handler Registry: For configurable chains, implement a registry that maps handler names to instances:
HandlerRegistry.register("auth", new AuthenticationHandler())
HandlerRegistry.register("rateLimit", new RateLimitHandler())
chain = ChainBuilder.fromConfig(["auth", "rateLimit"])This allows runtime configuration through configuration files or databases, a common requirement in enterprise applications.
Request Context Object: Instead of passing a simple request, use a rich context object that accumulates state as it moves through the chain:
class RequestContext:
originalRequest: Request
metadata: Map<string, any>
processingHistory: List<HandlerResult>
addMetadata(key, value)
recordHandlerResult(handlerName, success, message)This provides visibility into which handlers processed the request and allows handlers to share information without tight coupling. For example, an authentication handler might add user information to metadata that an authorization handler later consumes.
Common Variations
Conditional Chain: Handlers decide whether to forward based on request properties, not just whether they handled it. For example, a handler might say "I processed this request, but it still needs further handling, so I will forward it." This requires returning a result object (for example, HandlerResult with shouldContinue flag).
Bidirectional Chain: Handlers maintain references to both next and previous handlers, allowing requests to flow backward. This is useful in undo/redo scenarios or when a handler needs to send feedback to the previous handler. However, this increases complexity significantly and often indicates that Mediator would be more appropriate.
Priority-Based Chain: Instead of a fixed linear order, handlers have priorities, and the chain is dynamically sorted. This allows flexible handler ordering without manual reordering. Implement using a priority queue or sorted list during chain construction.
Testing Strategy
Unit Test Individual Handlers: Mock the next handler and verify that your handler correctly processes requests it can handle and forwards requests it cannot. Test both the processing logic and the forwarding logic independently.
Integration Test Full Chains: Build complete chains and verify end-to-end behavior. Test scenarios where the first handler processes, middle handler processes, last handler processes, and no handler processes. Verify the final state matches expectations.
Test Edge Cases: Empty chain (no handlers), single handler chain, very long chain (performance), null requests, requests that partially match multiple handlers.