Java design pattern deep dive
Chain of Responsibility Pattern in Java: 100 interview questions with professional answers.
Learn how the Chain of Responsibility pattern decouples a request's sender from the handler that ultimately processes it, how Servlet filters and Spring Security's filter chain are production-grade implementations of this idea, and how to design, order, terminate, and test a handler chain safely in real systems.
What makes a good Chain of Responsibility answer?
Interviewers want to see that you understand a chain as a runtime decision structure, not just a linked list of objects: each handler independently decides to act, skip, or forward, the chain has an explicit and testable order, and it always terminates somewhere.
| Approach | Use when | Watch out for |
|---|---|---|
| Hand-rolled linked-list chain (classic GoF) | You own the handler hierarchy, want each handler independently testable, and need explicit control over ordering and termination. | Forgetting the terminal handler, or forgetting a subclass call to the superclass's forwarding logic. |
Servlet Filter / FilterChain | You are inside a Servlet container and need cross-cutting request/response processing before a servlet, such as auth or compression. | Filter order is configuration, not code; a misordered web.xml or annotation can silently break security assumptions. |
Spring Security FilterChainProxy | You need a security-specific pipeline (authentication, authorization, CSRF) layered on top of, or alongside, ordinary Servlet filters. | Multiple security filter chains matched by request pattern can produce surprising precedence if patterns overlap. |
| Functional / lambda-based chain | Handlers are simple, stateless request-to-response transforms and you want composition without a class hierarchy. | Harder to insert conditional short-circuiting cleanly; error handling inside composed functions needs care. |
Topics
Interview questions and answers
Each answer gives the implementation direction, the trade-off to mention, and the production concern that makes the answer stronger.
1. Explain the Chain of Responsibility design pattern in Java and describe the real-world problem it solves when a request could be handled by one of several candidate objects.
Chain of Responsibility gives a request to the first object in a chain of potential handlers; each handler decides independently whether it can process the request, and if it cannot, it forwards the request to the next handler in the chain. It solves the problem of coupling a sender to one specific receiver when, in reality, any one of several receivers might be the right one depending on runtime conditions.
This shows up whenever you have an escalating or filtering pipeline: an expense approval that climbs management levels until someone has authority to approve it, or a request that passes through several validators until one rejects it or all pass.
abstract class Handler {
protected Handler next;
Handler setNext(Handler next) { this.next = next; return next; }
abstract void handle(Request request);
}
class AmountHandler extends Handler {
void handle(Request request) {
if (request.amount() < 100) {
System.out.println("Auto-approved");
} else if (next != null) {
next.handle(request);
}
}
}
2. Describe the classic GoF structure of Chain of Responsibility: the Handler abstract class or interface, ConcreteHandler subclasses, and the Client, and explain how each role interacts with the others.
The Handler declares the method to handle a request and typically holds a reference to the next handler in the chain. Each ConcreteHandler implements the handling logic for its own responsibility and, when it cannot fully satisfy the request, delegates to whatever the next reference points to. The Client builds or receives an already-built chain and only ever calls the first handler, never reaching into the middle of the chain directly.
interface Handler {
void setNext(Handler next);
void handle(Request request);
}
class ConcreteHandlerA implements Handler {
private Handler next;
public void setNext(Handler next) { this.next = next; }
public void handle(Request request) {
if (canHandle(request)) { /* process */ }
else if (next != null) { next.handle(request); }
}
private boolean canHandle(Request request) { return request.type().equals("A"); }
}
Crucially, no handler needs to know how many other handlers exist, what kind they are, or where it sits in the chain; it only knows its own responsibility and its immediate successor reference.
3. Walk through building a chain of handler objects at application startup, including where the wiring code should live so it stays testable and easy to reorder.
Chain construction, the act of calling setNext repeatedly to link handlers together, should happen in exactly one place, ideally a small factory method or a configuration class, rather than being scattered across the codebase. This keeps ordering an explicit, reviewable decision rather than an emergent side effect of initialization order.
class ApprovalChainFactory {
static Handler build() {
Handler manager = new ManagerApprovalHandler();
Handler director = new DirectorApprovalHandler();
Handler vp = new VpApprovalHandler();
manager.setNext(director);
director.setNext(vp);
return manager; // client only ever holds this reference
}
}
4. Explain the three choices available to a handler when it receives a request: handle and stop, handle and still forward, or skip and forward, and give a scenario for each.
Handle-and-stop is the most common: the handler fully processes the request and the chain ends there, as with an expense approval that a manager can authorize outright. Handle-and-forward lets a handler act (for example, logging or enriching the request) without claiming exclusive ownership, then still passes it along, which is how most Servlet filters behave. Skip-and-forward means the handler determines the request is not its responsibility at all and passes it on unchanged, as a currency-specific validator would do for a request in a currency it does not recognize.
void handle(Request request) {
log.info("auditing request {}", request.id()); // handle-and-forward: side effect, still passes on
if (next != null) next.handle(request);
}
5. Compare implementing the Handler role as an abstract class that stores the next reference versus a plain interface where each implementation manages its own next reference, and explain the trade-offs.
An abstract Handler base class centralizes the next-reference field and the default forwarding logic, so concrete handlers only override the decision logic and never duplicate boilerplate, at the cost of consuming Java's single inheritance slot. A plain interface leaves every implementation to store and forward through its own next reference, giving more flexibility (a handler could also implement unrelated interfaces) but risking inconsistent forwarding logic if one implementation forgets to call next.
// Abstract base: shared next-reference and default forwarding
abstract class AbstractHandler implements Handler {
protected Handler next;
public void setNext(Handler next) { this.next = next; }
protected void forward(Request request) { if (next != null) next.handle(request); }
}
Most production codebases favor the abstract base class specifically because it guarantees the forwarding call exists somewhere shared, reducing the "forgot to call next" class of bugs.
6. Explain the Gang of Four's canonical Logger example for Chain of Responsibility, where a message may need to be handled by a console logger, a file logger, and an error-mail logger depending on its severity level.
In the GoF book's motivating example, a logging request carries a severity level, and multiple loggers are chained so that a message flows through every logger whose configured threshold it meets or exceeds, rather than stopping at the first match; this is one of the few classic examples where several handlers in the chain legitimately act on the same request instead of exactly one claiming it.
abstract class Logger {
static final int INFO = 1, DEBUG = 2, ERROR = 3;
protected int mask;
protected Logger next;
Logger setNext(Logger next) { this.next = next; return next; }
void message(String msg, int severity) {
if (severity >= mask) { writeMessage(msg); }
if (next != null) { next.message(msg, severity); }
}
abstract void writeMessage(String msg);
}
7. Explain precisely how Chain of Responsibility decouples the sender of a request from the receiver, and why this matters more as a system grows the number of possible handlers over time.
The sender, the client code that issues the request, holds a reference only to the abstract handler type and only to the first link in the chain; it has no compile-time or runtime knowledge of which concrete handler, or how many handlers, will ultimately process the request. This means new handler types can be introduced, removed, or reordered without touching a single line of the sender's code.
As a system grows, the number of "who handles this" branches would otherwise accumulate as an ever-longer if-else or switch statement at every call site; Chain of Responsibility keeps that growth confined to the chain's construction point instead of spreading it through calling code.
8. Explain why an unterminated chain is a common production bug, and how to guarantee a request is never silently dropped by ensuring every chain ends in a default or terminal handler.
If the last handler in the chain checks a condition, fails to match, and simply returns because its next reference is null, the request disappears with no error, no log entry, and no visible symptom until someone notices the expected side effect never happened. This is one of the most common Chain of Responsibility bugs precisely because it produces no exception.
class DefaultHandler extends Handler {
void handle(Request request) {
log.warn("Unhandled request reached terminal handler: {}", request.id());
throw new UnsupportedOperationException("No handler processed request " + request.id());
}
}
// wire this as the last link in every chain, never leave next null on a conditional handler
9. Explain why the order in which handlers are linked matters, and describe a real bug that can occur when two handlers are accidentally swapped in the chain.
Chain order determines precedence: if a broad, catch-all handler is placed before a narrow, specific one, the specific handler may never run because the broad one already claimed and stopped the request. For example, placing a generic "unauthenticated user" rejection handler before a "public endpoint allowlist" handler would incorrectly reject requests to endpoints that should not require authentication at all.
// wrong order: generic rejection runs before the allowlist check
Handler chain = new RejectUnauthenticatedHandler();
chain.setNext(new PublicEndpointAllowlistHandler()); // never reached for public endpoints
// correct order
Handler correctChain = new PublicEndpointAllowlistHandler();
correctChain.setNext(new RejectUnauthenticatedHandler());
10. Design a fluent Builder-style API for assembling a Chain of Responsibility so that chain construction reads clearly and cannot produce an unterminated chain by accident.
A builder can accept handlers in order via a chained add() method, then require a mandatory terminal handler as part of a required build() step, making it a compile-time or at least a fail-fast runtime error to omit one.
class ChainBuilder {
private final java.util.List<Handler> handlers = new java.util.ArrayList<>();
ChainBuilder add(Handler handler) { handlers.add(handler); return this; }
Handler build(Handler terminal) {
handlers.add(terminal);
for (int i = 0; i < handlers.size() - 1; i++) {
handlers.get(i).setNext(handlers.get(i + 1));
}
return handlers.get(0);
}
}
11. Explain how the Servlet specification's Filter and FilterChain interfaces implement Chain of Responsibility, and how a filter decides whether to short-circuit the chain or continue it.
Each Filter receives the request, response, and a FilterChain object representing the remaining handlers. Calling chain.doFilter(request, response) forwards to the next filter (or ultimately the servlet); simply not calling it short-circuits the chain, which is exactly the handle-and-stop behavior of Chain of Responsibility.
public class AuthFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
if (!isAuthenticated((HttpServletRequest) req)) {
((HttpServletResponse) res).sendError(401);
return; // short-circuit: chain.doFilter is never called
}
chain.doFilter(req, res); // forward to the next filter or the servlet
}
}
12. Explain how Spring Security's security filter chain builds on top of the Servlet Filter chain to add authentication and authorization as a nested Chain of Responsibility.
Spring Security registers a single DelegatingFilterProxy (or equivalent) into the ordinary Servlet filter chain, which internally delegates to a FilterChainProxy managing its own ordered list of security-specific filters, such as UsernamePasswordAuthenticationFilter, CsrfFilter, and ExceptionTranslationFilter. This is a chain nested inside a chain: the outer Servlet chain treats Spring Security as one link, while internally that link runs its own Chain of Responsibility over dozens of security concerns.
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated());
return http.build();
}
13. Explain how Netty's ChannelPipeline uses Chain of Responsibility to process inbound and outbound network events through an ordered list of ChannelHandler instances.
A Netty ChannelPipeline holds an ordered, doubly-linked list of ChannelHandler instances. An inbound event, such as bytes arriving on the socket, travels forward through inbound handlers until one consumes it or it reaches the tail; an outbound event, such as a write request, travels backward through outbound handlers toward the socket. Each handler calls ctx.fireChannelRead(msg) (or the outbound equivalent) to forward to the next handler, mirroring exactly the explicit "call next" step of Chain of Responsibility.
public class FrameDecoderHandler extends ChannelInboundHandlerAdapter {
public void channelRead(ChannelHandlerContext ctx, Object msg) {
Object decoded = decode(msg);
if (decoded != null) ctx.fireChannelRead(decoded); // forward to next handler
}
}
14. Explain how the general concept of "middleware" in web frameworks maps onto Chain of Responsibility, and why middleware ordering is usually documented as one of the most important configuration decisions in such frameworks.
Middleware functions each wrap the next middleware (or the final route handler) and decide whether to run logic before, after, or instead of calling onward. This is structurally identical to Chain of Responsibility: each middleware is a handler, the "call next" function is the next-reference, and the final route handler is the terminal handler.
Ordering matters because middleware side effects compose sequentially: an authentication middleware must run before an authorization middleware, a request-logging middleware usually wants to run first to capture every request including ones later middleware rejects, and a response-compressing middleware needs to run after the body is fully generated.
15. Design an exception-handler chain where different exception types are matched by different handlers, falling back to a generic handler for unrecognized exception types.
Each handler in the chain declares which exception types (or predicate) it can handle; if the current exception does not match, it forwards to the next handler, ending in a catch-all handler that logs and returns a generic error response so no exception type is ever left completely unhandled.
abstract class ExceptionHandler {
protected ExceptionHandler next;
ExceptionHandler setNext(ExceptionHandler next) { this.next = next; return next; }
ErrorResponse handle(Exception ex) {
if (canHandle(ex)) return respond(ex);
if (next != null) return next.handle(ex);
throw new IllegalStateException("Unreachable: chain must end in a catch-all handler");
}
abstract boolean canHandle(Exception ex);
abstract ErrorResponse respond(Exception ex);
}
16. Design an expense approval workflow using Chain of Responsibility where requests escalate through manager, director, and VP levels based on the requested amount.
Each approval-level handler holds an approval limit; if the requested amount is within its limit, it approves and stops the chain, otherwise it forwards to the next, higher-authority handler. The VP level, as the final handler, either approves anything remaining or explicitly rejects amounts beyond company policy rather than silently doing nothing.
class LevelApprovalHandler extends Handler {
private final double limit;
private final String levelName;
LevelApprovalHandler(String levelName, double limit) { this.levelName = levelName; this.limit = limit; }
void handle(ExpenseRequest request) {
if (request.amount() <= limit) {
System.out.println(levelName + " approved " + request.amount());
} else if (next != null) {
next.handle(request);
} else {
throw new ApprovalLimitExceededException(request);
}
}
}
17. Design a validation pipeline using Chain of Responsibility where each validator checks one aspect of an incoming request and the pipeline collects all failures rather than stopping at the first one.
Unlike a handle-and-stop chain, a validation pipeline typically wants every validator to run regardless of earlier failures, so each handler always forwards, appending its own errors (if any) to a shared, mutable result object passed alongside the request.
class ValidationResult { final java.util.List<String> errors = new java.util.ArrayList<>(); }
abstract class Validator {
protected Validator next;
Validator setNext(Validator next) { this.next = next; return next; }
void validate(Order order, ValidationResult result) {
doValidate(order, result);
if (next != null) next.validate(order, result); // always forward, unlike handle-and-stop chains
}
abstract void doValidate(Order order, ValidationResult result);
}
18. Implement a functional-style Chain of Responsibility using Function composition instead of a class hierarchy, and explain how to preserve short-circuiting behavior.
Rather than subclassing a Handler, each step can be a Function<Request, Optional<Response>>; a small runner tries each function in order and stops at the first non-empty Optional, which reproduces handle-and-stop semantics without any inheritance.
List<Function<Request, Optional<Response>>> steps = List.of(
req -> req.amount() < 100 ? Optional.of(Response.autoApproved()) : Optional.empty(),
req -> req.isVip() ? Optional.of(Response.fastTracked()) : Optional.empty()
);
Optional<Response> result = steps.stream()
.map(step -> step.apply(request))
.filter(Optional::isPresent)
.findFirst()
.orElse(Optional.empty());
19. Implement a handle-and-forward chain using UnaryOperator composition, where every step transforms a request object and every step always runs, unlike the short-circuiting Function-based variant.
When every handler is guaranteed to run and mutate or enrich the same-shaped object, chaining a list of UnaryOperator<Request> together with andThen gives a clean, allocation-light pipeline, well suited to enrichment stages like adding a trace ID, a timestamp, and a normalized currency before the request reaches business logic.
UnaryOperator<Request> addTraceId = req -> req.withTraceId(java.util.UUID.randomUUID().toString());
UnaryOperator<Request> addTimestamp = req -> req.withTimestamp(java.time.Instant.now());
UnaryOperator<Request> normalizeCurrency = req -> req.withCurrency(req.currency().toUpperCase());
UnaryOperator<Request> pipeline = addTraceId.andThen(addTimestamp).andThen(normalizeCurrency);
Request enriched = pipeline.apply(incoming);
20. Explain the difference between Chain of Responsibility and Decorator, given both forward calls through a series of wrapping objects, and provide a code example that highlights the structural distinction.
Both patterns pass a call through a series of linked objects, but a Decorator chain always runs every decorator, in a fixed nesting order, and each one always adds behavior around the same call; a Chain of Responsibility chain runs only as many handlers as needed until one claims the request, and any handler may stop the chain entirely.
// Decorator: every layer always runs, adds behavior, doesn't "choose" to skip
interface Coffee { double cost(); }
class MilkDecorator implements Coffee {
private final Coffee inner;
MilkDecorator(Coffee inner) { this.inner = inner; }
public double cost() { return inner.cost() + 0.5; } // always adds 0.5, always delegates
}
// Chain of Responsibility: a handler can stop the chain outright
abstract class Handler {
protected Handler next;
abstract void handle(Request request); // may or may not call next.handle(request)
}
21. Explain the relationship between Chain of Responsibility and the Command pattern, and describe a design where each link in the chain is itself encapsulated as a Command object.
Command encapsulates a request as an object with its own execute() method, independent of who invokes it; Chain of Responsibility decides who handles a request. The two combine naturally when each handler in the chain, upon deciding to act, delegates the actual work to a Command object, which keeps the "who should respond" decision (Chain of Responsibility) separate from "what happens when they respond" (Command), and lets commands be queued, logged, or undone independently of the chain that selected them.
class ApprovalHandler extends Handler {
private final Command approveCommand;
ApprovalHandler(Command approveCommand) { this.approveCommand = approveCommand; }
void handle(Request request) {
if (canApprove(request)) approveCommand.execute(request);
else if (next != null) next.handle(request);
}
}
22. Explain the difference between Chain of Responsibility and Observer, particularly around whether a request is broadcast to every interested party or passed along until exactly one (or a few) handle it.
Observer broadcasts a notification to every registered observer unconditionally; none of them decide whether another observer gets to see the event, and the subject has no concept of "stopping" the notification once emitted. Chain of Responsibility passes a single request sequentially, and each handler can consume it, ending the chain right there so later handlers never even see it, which makes Chain of Responsibility fundamentally about selective, sequential ownership rather than broadcast.
23. Explain the difference between Chain of Responsibility and Mediator, since both patterns can reduce direct coupling between many collaborating objects.
Mediator centralizes communication: a single mediator object knows about many colleague objects and coordinates interactions between them, and colleagues talk only to the mediator, never to each other. Chain of Responsibility has no central coordinator at all; each handler only knows about its own successor, and control passes linearly down the chain rather than being routed through one central hub.
Put differently, Mediator decouples colleagues from each other via a hub-and-spoke topology, while Chain of Responsibility decouples a sender from a receiver via a linear pass-along topology; both avoid tight coupling, but the shape of the resulting object graph is fundamentally different.
24. Explain how Chain of Responsibility compares to using pure polymorphism (a single virtual method call) or the Visitor pattern when deciding how a request should be routed to the correct handling logic.
Plain polymorphism works when the object receiving the call already knows, by its own concrete type, exactly how to handle the request; there is no need to ask a series of other objects first. Visitor is useful when you need to add new operations across a fixed set of element types without modifying those types, dispatching based on the element's type via double dispatch. Chain of Responsibility is different from both because the deciding factor is not the type of a single object but a runtime condition evaluated in sequence across a variable, potentially reorderable set of independent handlers.
Choose Chain of Responsibility specifically when the "who should handle this" question cannot be answered by looking at one object's type alone, and instead depends on trying candidates in a meaningful order until one accepts.
25. Explain the difference between Chain of Responsibility and Template Method, since both structure a sequence of steps, and clarify why a fixed, always-executed sequence is better modeled with Template Method.
Template Method defines a fixed algorithm skeleton in a base class, with subclasses overriding specific steps, but the overall sequence and the fact that every step runs is baked in at compile time by a single class hierarchy. Chain of Responsibility instead assembles an ordered list of independent objects at runtime, any of which can stop the sequence early, and the set of participants can change without touching a shared algorithm class.
If your steps always run in the same fixed order and never conditionally skip the rest, a Template Method (or simply sequential method calls) is simpler and easier to follow than standing up a chain of handler objects purely for the sake of using a well-known pattern name.
26. Explain how Chain of Responsibility differs from Strategy, given both let you plug in different behavior objects, and describe a scenario where developers confuse the two.
Strategy selects exactly one algorithm object to run for a given context, chosen once (often via configuration or a factory), and that one strategy fully executes the operation. Chain of Responsibility tries a sequence of candidate handlers, potentially several in a row, until one (or several, in the multi-handler variant) actually processes the request; the number of participants involved is not fixed in advance the way a single strategy selection is.
Confusion arises when a "chain" only ever has one real candidate handler active at a time based on configuration, which is really just Strategy wearing a Chain of Responsibility skeleton; a good interview answer names this and explains that Strategy is the simpler, more honest description when there is no genuine try-then-forward sequence happening.
27. Discuss the thread-safety concerns of a Chain of Responsibility where individual handler instances are shared across concurrent requests and hold mutable instance state.
Handlers are usually built once at startup and shared across every request that flows through the chain, so any mutable instance field on a handler becomes shared mutable state across threads, exactly the kind of state that needs synchronization or, better, elimination. The fix is almost always to keep handlers stateless and pass all per-request data through the request object (or an accompanying context object) rather than storing it on the handler itself.
// Unsafe: mutable field shared across concurrent requests
class CountingHandler extends Handler {
private int callCount; // race condition under concurrent calls
void handle(Request request) { callCount++; /* ... */ }
}
// Safe: request-scoped counters live on the request/context, not the handler
class StatelessCountingHandler extends Handler {
void handle(Request request) { request.context().incrementHandlerCount(); }
}
28. Discuss the performance implications of a very long Chain of Responsibility, and explain how ordering handlers by likelihood of matching can meaningfully reduce average-case latency.
Each unmatched handler a request passes through before being claimed adds its own check's cost, however small, to the total latency; with a long chain and a high request volume, this adds up. Since Chain of Responsibility already makes ordering an explicit, controllable decision, placing handlers that match the most common cases earliest in the chain reduces the average number of handlers a typical request must traverse, an optimization that is invisible at the code level but measurable under load.
This should be balanced against correctness constraints: a handler that must run first for security or validation reasons (such as authentication) cannot be reordered purely for a latency win if doing so would change behavior.
29. Explain how to build a dynamically reconfigurable Chain of Responsibility in a Spring application by autowiring a List of Handler beans and ordering them declaratively with @Order.
Spring will autowire every bean implementing an interface into a List in the order determined by each bean's @Order annotation (or by implementing Ordered), letting the framework assemble the chain's order from configuration rather than a hand-wired factory method, and letting new handler beans join the chain automatically just by being registered in the application context.
@Order(1)
@Component
class AuthenticationHandler implements RequestHandler { /* ... */ }
@Order(2)
@Component
class AuthorizationHandler implements RequestHandler { /* ... */ }
@Service
class RequestPipeline {
private final List<RequestHandler> handlers; // injected in @Order sequence
RequestPipeline(List<RequestHandler> handlers) { this.handlers = handlers; }
void process(Request request) {
for (RequestHandler handler : handlers) {
if (handler.handle(request)) return; // handled, stop iterating
}
}
}
30. Design a resilience pipeline combining circuit-breaker and retry handlers as a Chain of Responsibility around an outbound call to an unreliable downstream service.
Each resilience concern becomes its own handler: a circuit-breaker handler short-circuits immediately if the downstream service has recently failed too often, a retry handler wraps the actual call with bounded retry attempts, and the actual outbound call is the terminal handler. Layering them as a chain keeps each concern testable independently rather than tangled together in one giant method.
class CircuitBreakerHandler extends Handler {
private final CircuitBreaker breaker;
void handle(Request request) {
if (breaker.isOpen()) { throw new CircuitOpenException(); }
if (next != null) next.handle(request);
}
}
31. Explain how OkHttp's Interceptor chain is a real-world implementation of Chain of Responsibility for HTTP client requests, including the distinction between application interceptors and network interceptors.
Each OkHttp Interceptor receives a Chain object and must call chain.proceed(request) to forward to the next interceptor (or the actual network call), optionally transforming the request beforehand or the response afterward, exactly mirroring the explicit next-call style of Chain of Responsibility. Application interceptors run once regardless of retries or redirects, while network interceptors run once per actual network attempt, so they see redirected requests and retried requests separately.
class LoggingInterceptor implements Interceptor {
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
long start = System.nanoTime();
Response response = chain.proceed(request); // forward to the next interceptor
log.info("{} took {}ms", request.url(), (System.nanoTime() - start) / 1_000_000);
return response;
}
}
32. Explain how Feign client RequestInterceptor and ResponseInterceptor chains fit the Chain of Responsibility pattern when building a declarative HTTP client in a Spring Cloud microservice.
Feign's RequestInterceptor instances are each given the outgoing request template and can mutate it (adding headers, propagating a trace ID) before it is sent; multiple interceptors registered on a client run in sequence, each seeing the result of the previous interceptor's modifications, which is the handle-and-forward variant of Chain of Responsibility applied to outbound request enrichment rather than routing.
@Bean
RequestInterceptor traceIdInterceptor() {
return template -> template.header("X-Trace-Id", MDC.get("traceId"));
}
33. Explain how Spring WebFlux's WebFilter chain adapts Chain of Responsibility to a reactive, non-blocking programming model using Mono instead of a synchronous next() call.
A WebFilter receives a WebFilterChain and must call chain.filter(exchange), which returns a Mono<Void> representing the eventual completion of the rest of the chain, rather than blocking synchronously; the filter composes its own logic around that Mono using reactive operators instead of sequential statements.
@Component
class TraceWebFilter implements WebFilter {
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
exchange.getResponse().getHeaders().add("X-Trace-Id", java.util.UUID.randomUUID().toString());
return chain.filter(exchange) // forward, wrapped in reactive composition
.doOnSuccess(v -> log.info("request completed"));
}
}
34. Describe how you would debug a Chain of Responsibility where a handler silently swallows an exception, causing requests to disappear from downstream processing with no visible error.
The classic symptom is that a request enters the chain and simply never produces any downstream effect, with no exception surfacing anywhere, because a handler's try-catch block caught an exception and returned without rethrowing, logging, or forwarding. The fix during debugging is to add temporary logging at the entry and exit of every handler, or to instrument the chain with a wrapping decorator that logs before and after each handler's call, quickly isolating which handler stopped emitting a downstream effect.
class NoisyHandlerWrapper extends Handler {
private final Handler delegate;
NoisyHandlerWrapper(Handler delegate) { this.delegate = delegate; }
void handle(Request request) {
log.debug("entering {}", delegate.getClass().getSimpleName());
delegate.handle(request);
log.debug("exiting {}", delegate.getClass().getSimpleName());
}
}
35. Explain the common bug of forgetting to call the next handler after a conditional check fails, and show the corrected version of the code.
A handler that checks a condition and, if it does not match, simply falls through to the end of its method without calling next.handle(request), silently drops every request that does not match its own narrow condition, which is functionally the same bug as an unterminated chain but caused by a missing forward call rather than a missing terminal handler.
// Buggy: no else branch calls next
void handle(Request request) {
if (request.type().equals("REFUND")) {
processRefund(request);
}
// BUG: non-refund requests silently vanish here
}
// Fixed
void handle(Request request) {
if (request.type().equals("REFUND")) {
processRefund(request);
} else if (next != null) {
next.handle(request);
}
}
36. Explain how an infinite loop can accidentally occur in a Chain of Responsibility if the chain is misconfigured to cycle back to an earlier handler, and how to guard against it.
If chain-building code accidentally sets a later handler's next reference back to an earlier handler already in the chain, perhaps due to a copy-paste error or a factory method reusing a variable incorrectly, the request loops indefinitely between the same handlers, typically manifesting as a StackOverflowError from unbounded recursive calls or a hung thread in an iterative implementation.
// Bug: accidental cycle
Handler a = new HandlerA();
Handler b = new HandlerB();
a.setNext(b);
b.setNext(a); // cycle! request bounces between a and b forever
Guarding against this can be as simple as an assertion in the chain-building factory that verifies no handler instance appears twice while walking the built chain, or, defensively, passing a visited-handler set alongside the request in development/test builds to detect a repeat visit immediately.
37. Describe a bug where a handler mutates shared request state in a way that unexpectedly affects a later handler's decision, and explain how to prevent this class of bug.
If handlers pass a single mutable request object down the chain and an earlier handler mutates a field the later handler also reads, the later handler's behavior becomes dependent on execution order in ways that are easy to get wrong, especially once the chain is reordered for an unrelated reason. For example, an earlier handler that "normalizes" an amount field in place could cause a later fraud-detection handler to compare against an already-modified value instead of the original.
// Safer: return a new, immutable request rather than mutating the shared one in place
Request normalized = request.withAmount(normalize(request.amount()));
next.handle(normalized); // later handlers see an explicit, traceable transformation
38. Explain how to unit test an individual handler in isolation, without needing to construct the entire chain, by mocking the next handler.
Since a handler only ever calls next.handle(request) through an abstract or interface reference, a test can inject a mock next-handler and assert both that the handler under test made the correct decision (handled versus forwarded) and, when forwarding, that it called the mock with the expected (possibly transformed) request.
@Test
void forwardsWhenAmountExceedsLimit() {
Handler next = mock(Handler.class);
LevelApprovalHandler handler = new LevelApprovalHandler("Manager", 1000);
handler.setNext(next);
handler.handle(new ExpenseRequest(5000));
verify(next).handle(any(ExpenseRequest.class));
}
39. Explain how to write an end-to-end test for the fully assembled chain that verifies correct behavior across handler boundaries, complementing the isolated per-handler unit tests.
Per-handler unit tests verify each link's own logic in isolation but cannot catch ordering mistakes or integration issues between handlers; an end-to-end test builds the real chain via the same factory production code uses, feeds it representative requests for each expected outcome, and asserts the final observable result, such as which approval level actually authorized a given amount.
@Test
void amountOf1500EscalatesPastManagerToDirector() {
Handler chain = ApprovalChainFactory.build(); // real production wiring
ExpenseRequest request = new ExpenseRequest(1500);
ApprovalResult result = chain.handleAndReturnResult(request);
assertThat(result.approvedBy()).isEqualTo("Director");
}
Keep a small number of these ordering-sensitive end-to-end tests alongside the larger number of fast, isolated per-handler unit tests, since the end-to-end tests are the ones that actually catch a misordered chain.
40. Design a priority-based Chain of Responsibility where handlers are consulted in order of a numeric priority field rather than a fixed, hand-wired sequence, and explain when this flexibility is worth the added complexity.
Instead of manually calling setNext in a fixed sequence, handlers can each expose a priority value, and the chain-building code sorts them before wiring the next-references, letting new handlers be inserted anywhere in the priority range without touching existing wiring code.
List<Handler> sorted = handlers.stream()
.sorted(Comparator.comparingInt(Handler::priority))
.toList();
for (int i = 0; i < sorted.size() - 1; i++) {
sorted.get(i).setNext(sorted.get(i + 1));
}
This flexibility earns its complexity when handlers genuinely come from different modules or plugins that should not need to know about each other's exact position, but it is overkill for a small, stable chain where an explicit hand-wired order is clearer to read.
41. Explain how to support dynamic, runtime reconfiguration of a live Chain of Responsibility, such as adding or removing a handler while the application is running, and the concurrency hazards involved.
Rebuilding the chain and swapping the reference the client holds atomically, for example via an AtomicReference<Handler> updated with a freshly built chain, avoids the hazard of mutating next-references on live handler objects while requests are actively flowing through them, which could otherwise let one in-flight request see a half-updated chain.
class ReconfigurableChain {
private final java.util.concurrent.atomic.AtomicReference<Handler> head =
new java.util.concurrent.atomic.AtomicReference<>(ApprovalChainFactory.build());
void reload() { head.set(ApprovalChainFactory.build()); } // atomic swap, no partial state visible
void handle(Request request) { head.get().handle(request); }
}
42. Design an authentication-then-authorization pipeline as a Chain of Responsibility, and explain why these two concerns must run in that specific order and never be reordered.
Authentication establishes who the caller is; authorization decides what that identity is permitted to do. Authorization logically depends on an established identity, so an authorization handler placed before the authentication handler either has no identity to check against or, worse, might default to an insecure "allow" outcome when identity information is simply absent rather than explicitly denied.
Handler chain = new AuthenticationHandler();
chain.setNext(new AuthorizationHandler()); // must run second, never first
chain.getNext().setNext(new BusinessLogicHandler());
43. Explain the difference between a chain designed so that exactly one handler ever claims a request versus a chain designed so that multiple handlers may all legitimately act on the same request, and give an example of each.
A single-owner chain, like the expense-approval example, is designed so exactly one handler's decision determines the outcome and the chain stops there. A multi-handler (broadcast-like) chain, like the GoF logger example, is designed so every handler whose condition matches performs its own side effect and the request still keeps forwarding regardless, since none of them is meant to have exclusive ownership.
A good interview answer states explicitly which variant a given design uses, since conflating the two is a common source of bugs: assuming a chain stops at the first match when it was actually built to let several handlers act, or vice versa.
44. Building on the Command-pattern combination, explain how you would let each link in the chain wrap a fully independent, queueable, undoable Command rather than inline handling logic.
Each handler in the chain checks whether it applies, and if so, constructs and executes a Command object representing the actual action, which can then be pushed onto an undo stack, logged as an audit entry, or queued for asynchronous execution, entirely independent of the chain's own routing logic.
class RefundHandler extends Handler {
private final CommandInvoker invoker;
void handle(Request request) {
if (isRefundEligible(request)) {
invoker.executeAndRecord(new RefundCommand(request)); // undoable, auditable
} else if (next != null) {
next.handle(request);
}
}
}
45. Explain how Chain of Responsibility differs from the Interpreter pattern, since both can involve a series of objects each processing part of an input, and clarify when each is the right model.
Interpreter models a grammar: a tree of expression objects, each representing a grammar rule, cooperatively evaluates an entire input according to fixed composition rules defined by the language's structure. Chain of Responsibility models a decision sequence: a flat, linear list of independent handlers, each evaluating whether it alone is responsible for the whole request, with no shared grammar or tree structure connecting them.
Use Interpreter when you are genuinely parsing or evaluating a structured language or expression grammar; use Chain of Responsibility when you are routing one opaque request to whichever handler among several unrelated candidates should process it.
46. Explain the different ways Servlet Filter order can be configured, such as web.xml ordering, @WebFilter annotations, and Spring Boot's FilterRegistrationBean, and the pitfalls of relying on each.
In a traditional web.xml, filters run in the order their <filter-mapping> elements appear in the file, which is explicit but easy to overlook during a merge. The @WebFilter annotation gives no reliable way to control order at all when combined with component scanning, since discovery order is not guaranteed. Spring Boot's FilterRegistrationBean lets you set an explicit order value programmatically, which is the most reliable and reviewable approach in a Spring application.
@Bean
FilterRegistrationBean<AuthFilter> authFilter() {
FilterRegistrationBean<AuthFilter> registration = new FilterRegistrationBean<>(new AuthFilter());
registration.setOrder(1); // explicit, reviewable ordering
return registration;
}
47. Explain the internal structure of Spring Security's FilterChainProxy, including how it matches an incoming request to one of potentially several configured SecurityFilterChain instances before running that chain's own internal filter list.
FilterChainProxy holds an ordered list of SecurityFilterChain definitions, each associated with a request matcher; for each incoming request, it evaluates the matchers in order and selects the first matching chain, then runs that chain's entire ordered list of security filters as a nested Chain of Responsibility, rather than merging all configured chains into one flat list.
@Bean
@Order(1)
SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
http.securityMatcher("/api/**").authorizeHttpRequests(a -> a.anyRequest().authenticated());
return http.build();
}
@Bean
@Order(2)
SecurityFilterChain defaultChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(a -> a.anyRequest().permitAll());
return http.build();
}
@Order) ever actually runs for a matching request, which can silently bypass intended rules on the second.48. Explain the distinction between inbound and outbound handlers in a Netty ChannelPipeline, and why the pipeline effectively runs two separate Chain of Responsibility sequences in opposite directions.
Inbound handlers process events flowing from the network toward the application (bytes read, decoded messages) and are traversed from the head of the pipeline toward the tail; outbound handlers process events flowing from the application toward the network (writes, connects) and are traversed from the tail toward the head. A single pipeline therefore behaves as two independently ordered chains sharing the same list of handler entries, each handler participating in whichever direction(s) its interface implements.
public class LengthPrefixEncoder extends MessageToByteEncoder<ByteBuf> { // outbound
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) {
out.writeInt(msg.readableBytes());
out.writeBytes(msg);
}
}
49. Explain, in general terms applicable across frameworks, why "the middleware pattern" is best understood as an application of Chain of Responsibility rather than a distinct pattern in its own right.
Across virtually every framework that offers "middleware," the shape is identical: an ordered list of functions or objects, each given the request (and often a reference to the rest of the pipeline), each free to inspect, transform, short-circuit, or forward. This is precisely Chain of Responsibility's structure and intent, expressed with framework-specific naming rather than a genuinely new set of design rules.
Recognizing this lets you transfer lessons across frameworks: the same concerns about ordering, termination, and testing individual links in isolation apply whether you call the mechanism middleware, filters, interceptors, or handlers.
50. Design a context-sensitive help system, similar to the Gang of Four's own motivating discussion, where a help request escalates from a specific UI widget up through its containing dialog and application if no more specific help text is available.
Each UI element, from the smallest widget up to the top-level application window, implements a handler that checks whether it has specific help content registered for the current context; if not, it forwards the request to its containing parent, naturally mirroring the UI's own containment hierarchy as the chain's next-references.
abstract class HelpHandler {
protected HelpHandler parent;
String getHelp(String topic) {
String help = ownHelpText(topic);
if (help != null) return help;
return parent != null ? parent.getHelp(topic) : "No help available";
}
abstract String ownHelpText(String topic);
}
51. Explain the benefits of having each handler's handle method return an Optional<Response> instead of void, to explicitly signal whether it processed the request.
A void-returning handler makes "did anyone handle this" implicit and hidden, often discoverable only through a side effect. Returning Optional<Response> makes the outcome an explicit, inspectable value: an empty Optional means the handler declined and the caller (or a driving loop) should try the next handler, while a present Optional both signals completion and carries the actual result, all without relying on exceptions or shared mutable flags.
Optional<Response> handle(Request request) {
if (!canHandle(request)) return Optional.empty();
return Optional.of(process(request));
}
// driving loop
for (Handler h : handlers) {
Optional<Response> result = h.handle(request);
if (result.isPresent()) return result.get();
}
52. Compare implementing the chain as a true linked list of handler objects each holding a next reference versus simply iterating over a List of Handler objects in a driving loop, and discuss the trade-offs.
The classic GoF linked-list style keeps forwarding logic distributed inside each handler, which allows a handler to make forwarding conditional on complex, handler-specific logic (skip two handlers ahead, forward to a special-case handler out of normal sequence). Iterating over a plain List<Handler> in a driving loop centralizes the "try the next one" logic in one place, which is simpler to reason about and test but assumes a strictly linear, uniform iteration with no handler-specific forwarding logic.
// List + loop: simpler, but only supports strictly sequential try-next semantics
for (Handler h : handlers) {
if (h.tryHandle(request)) break;
}
Most modern Java codebases favor the list-and-loop style specifically because it is easier to test, reorder via configuration, and reason about, reserving the true linked-list style for cases needing handler-specific forwarding logic.
53. Compare a recursive implementation of chain traversal, where each handler calls the next handler's method directly, against an iterative implementation using an explicit loop, and discuss stack-depth and readability trade-offs.
Recursive traversal, where each handler literally calls next.handle(request), mirrors the GoF description closely and keeps forwarding logic local to each handler, but a sufficiently long chain, or a chain that grows unexpectedly at runtime, risks a StackOverflowError since each forward adds a stack frame. An iterative driving loop over a list avoids stack growth entirely regardless of chain length, at the cost of requiring handlers to communicate "handled" status back to the loop rather than simply not forwarding.
For any chain whose length is data-driven or could grow large (as opposed to a small, fixed number of well-known handlers), the iterative style is the safer default.
54. Explain how logging framework appender chains, such as Log4j2's appender references or Logback's appender-ref configuration, resemble Chain of Responsibility, and where the resemblance breaks down.
A logging configuration that attaches multiple appenders to a logger, each with its own level threshold, resembles the GoF Logger example: a single log event may be handled (written) by several appenders that each independently decide, based on level and filters, whether to act. The resemblance breaks down because most logging frameworks do not implement a literal forwarding chain between appender objects; instead, the framework itself dispatches the same event to every configured appender directly, which is closer to Observer's broadcast semantics than to a true handler-to-handler forwarding chain.
55. Explain the structural differences between a Servlet Filter and a textbook Chain of Responsibility Handler, particularly around how the "next" reference is passed and who owns chain construction.
A textbook Handler stores its own next-reference as a field, set once during chain construction and reused across every request. A Servlet Filter instead receives a fresh FilterChain object as a method parameter on every single call, and that chain object, managed entirely by the Servlet container, is what actually holds the ordering and advances an internal index; the filter itself never stores or owns a next-reference at all.
This is a meaningful implementation variant worth naming explicitly: it decouples the filter instance completely from chain-position bookkeeping, letting the same filter instance be safely reused across many concurrently in-flight chains without any per-request mutable next-reference state.
56. Explain how Spring MVC's @ExceptionHandler methods spread across multiple @ControllerAdvice classes resemble a Chain of Responsibility for exception handling, and how Spring actually resolves which handler applies.
Multiple @ControllerAdvice classes, each with their own @ExceptionHandler methods for different exception types, can look like independent handlers competing to process an exception. In practice, Spring does not walk them as a literal linked chain at request time; it resolves the single most specific matching @ExceptionHandler method ahead of time based on exception type specificity and controller-advice ordering (@Order), then invokes only that one method, which is closer to a lookup-table dispatch than a true sequential try-then-forward chain.
@ControllerAdvice
@Order(1)
class ValidationAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
ResponseEntity<?> handleValidation(MethodArgumentNotValidException ex) { /* ... */ }
}
57. Explain how to combine Jakarta Bean Validation annotations with a custom Chain of Responsibility validator pipeline, using Bean Validation for simple field constraints and the chain for cross-field or business-rule validation.
Bean Validation annotations such as @NotNull and @Size are well suited to simple, declarative, per-field constraints, but business rules spanning multiple fields, external lookups, or context-dependent logic often fit more naturally as explicit validator handlers in a chain, keeping the two layers separate rather than forcing complex logic into a custom annotation.
@Valid OrderRequest request; // Bean Validation: structural constraints, runs first
// Chain of Responsibility: cross-field / business-rule validation, runs after
ValidationResult result = new ValidationResult();
businessValidatorChain.validate(request, result);
if (!result.errors.isEmpty()) throw new BusinessValidationException(result.errors);
58. What are the drawbacks of overusing Chain of Responsibility in a codebase, and what signals suggest a chain has grown too long or too implicit to reason about safely?
A very long chain means understanding what happens to any given request requires mentally simulating every handler in sequence, since ownership is determined at runtime rather than visible from any single call site; this cost grows roughly linearly with chain length. A common signal that a chain has grown unhealthy is when developers can no longer confidently predict, without running the code or reading every handler, which handler will actually claim a given request.
Other overuse signals include handlers that exist purely to route to sub-chains without doing any real work themselves (indicating the chain should probably be flattened or restructured), and handlers whose forwarding conditions have become so intertwined that reordering any two of them changes behavior in surprising ways.
59. Describe a production performance regression caused by a long Chain of Responsibility where several early handlers each perform blocking I/O before ultimately forwarding to the handler that actually matters for most requests.
If, say, five handlers each make a small blocking database or cache lookup purely to decide "not my responsibility, forward," and the sixth handler is the one that actually applies to 95% of traffic, every one of those requests pays the latency cost of five unnecessary I/O round trips before reaching the handler that mattered, even though none of those checks changed the outcome.
The fix is usually to reorder the chain so the most frequently matching handler runs first (as discussed in the ordering-for-performance question), or to replace a per-request I/O check with a cached, in-memory decision where the underlying data changes infrequently.
60. Implement a Chain of Responsibility for a reactive pipeline where each handler returns a Mono<Optional<Response>> and the chain must short-circuit on the first non-empty result without blocking any thread.
Reactive short-circuiting cannot use a simple imperative loop, since each handler's result arrives asynchronously; instead, fold the handlers into a sequence of Mono operations chained with switchIfEmpty, so each subsequent handler's Mono is only subscribed to if the previous one completed empty, preserving lazy, non-blocking short-circuit semantics.
Mono<Response> result = handlerA.handle(request)
.switchIfEmpty(Mono.defer(() -> handlerB.handle(request)))
.switchIfEmpty(Mono.defer(() -> handlerC.handle(request)));
61. Discuss whether "interceptor" and "Chain of Responsibility" describe the same thing, or whether interceptor is better understood as a variant with a distinct emphasis, and explain the distinction precisely.
Interceptor implementations, such as OkHttp's or Spring's HandlerInterceptor, are structurally an application of Chain of Responsibility, but the term "interceptor" carries an added emphasis: each participant is expected to run for essentially every request (handle-and-forward being the default), primarily to observe or lightly modify the request/response, rather than to compete for exclusive ownership of who ultimately "handles" it.
Chain of Responsibility as a general pattern name makes no such assumption; some chains are built precisely so that only one handler ever claims a request. So "interceptor" is best described as a Chain of Responsibility variant specialized for cross-cutting, largely non-exclusive concerns, rather than a synonym for the pattern as a whole.
62. Design a simple rule engine for fraud detection using Chain of Responsibility, where each rule handler can flag a transaction and the chain continues collecting flags from every applicable rule.
Like the validation-pipeline variant, a fraud rule engine typically wants every rule evaluated rather than stopping at the first match, since a transaction might trip multiple independent rules whose combined signal matters for the final risk score; each rule handler appends its own flag (if triggered) to a shared, accumulating result and always forwards.
class VelocityRuleHandler extends RuleHandler {
void evaluate(Transaction tx, FraudSignals signals) {
if (tx.recentTransactionCount() > 10) signals.add("HIGH_VELOCITY");
if (next != null) next.evaluate(tx, signals); // always forward, collecting signals
}
}
63. Compare implementing retry logic as an explicit handler inside a Chain of Responsibility versus implementing it as an aspect using Spring AOP or a resilience library's annotation, and discuss the trade-offs.
A retry handler inside an explicit chain keeps retry logic visible in the same place as the rest of the request-processing pipeline, making the interaction between retries and other handlers (such as a circuit breaker positioned before or after it) explicit and orderable. An AOP-based or annotation-driven retry (such as Resilience4j's @Retry) is more concise at the call site and requires no manual chain wiring, but hides the retry's interaction with other cross-cutting concerns behind proxy magic, which can be harder to reason about when several such annotations stack on the same method.
@Retry(name = "paymentService", fallbackMethod = "fallback")
PaymentResult charge(PaymentRequest request) { /* ... */ }
Prefer the explicit chain when retry ordering relative to other handlers is a first-class design concern; prefer the annotation-driven approach for simple, self-contained retry needs on an otherwise ordinary method.
64. Explain the difference between using Chain of Responsibility and a State Machine to model a multi-step workflow, such as an order moving through several processing stages.
A state machine explicitly models a finite set of named states and the legal transitions between them, including the ability to move backward, branch conditionally to non-adjacent states, or re-enter a state; the current state is itself meaningful, persisted data. A Chain of Responsibility instead models a single forward pass through a sequence of independent handlers deciding "is this my responsibility," with no inherent concept of the request's "state" as first-class, queryable data, and no native support for moving backward.
Choose Chain of Responsibility for a linear "try each candidate until one claims it" decision; choose a state machine when the workflow has genuinely named states, conditional transitions between non-adjacent stages, or a need to persist and query "what state is this order currently in" independently of any handler class.
65. Explain how you would use a priority queue, rather than a fixed list, to order handlers dynamically as their priorities change at runtime due to configuration updates.
If handler priorities can change while the application is running (for example, via a feature-flag-driven priority override), rebuilding the ordered chain from a PriorityQueue keyed on each handler's current priority value, then re-wiring next-references from the sorted result, lets the chain's effective order track configuration changes without hand-maintaining sorted insertion logic.
PriorityQueue<Handler> queue = new PriorityQueue<>(Comparator.comparingInt(Handler::currentPriority));
queue.addAll(allHandlers);
List<Handler> ordered = new ArrayList<>();
while (!queue.isEmpty()) ordered.add(queue.poll());
// re-wire next-references from `ordered`, as in the reconfigurable-chain example
66. Explain the benefits of passing an immutable request object through a Chain of Responsibility, where any transformation produces a new object rather than mutating the original in place.
An immutable request eliminates an entire category of bugs where a handler's mutation unexpectedly affects a later handler's view of the data (as discussed in the shared-mutable-state question), and it makes debugging vastly easier, since logging the request object at any point in the chain reliably reflects exactly the state that handler saw, with no risk that a later handler further along has already changed it by the time you inspect a log.
record Request(String id, java.math.BigDecimal amount, String currency) {
Request withAmount(java.math.BigDecimal newAmount) {
return new Request(id, newAmount, currency); // new instance, original untouched
}
}
67. Explain how to configure a Chain of Responsibility's handler order declaratively, such as via a YAML or properties file, rather than hard-coding the order in Java, and the trade-offs of doing so.
Handlers can be registered by name in a lookup map (or as Spring beans looked up by bean name), and a configuration file lists the desired handler names in order; chain-building code reads that ordered list and wires the corresponding handler instances accordingly, letting operators change handler order via configuration deployment rather than a code change.
chain:
order:
- authenticationHandler
- authorizationHandler
- businessLogicHandler
The trade-off is that a purely declarative order sacrifices some compile-time safety, since a typo in a handler name, or an accidental omission of a security-critical handler, is now a runtime configuration error rather than a compilation failure; mitigate this with a startup-time validation step that fails fast if a required handler is missing from the configured order.
68. Extend the fluent Builder-style chain-construction API discussed earlier to also support named insertion points, letting a caller add a handler "before" or "after" a specific existing handler by name.
Tracking handlers by name in an ordered list (rather than only by position) lets a builder expose addBefore(name, handler) and addAfter(name, handler) methods that locate the reference handler's index and insert at the correct relative position, which is especially useful for plugin-style systems where a third-party module needs to insert its own handler relative to a well-known core handler without knowing the chain's full absolute order.
ChainBuilder builder = new ChainBuilder()
.add("auth", new AuthenticationHandler())
.add("business", new BusinessLogicHandler())
.addAfter("auth", "audit", new AuditHandler()); // inserted between auth and business
69. Show how to use Mockito to verify that a handler correctly stops the chain (never calls the next handler) when it successfully processes a request, as distinct from verifying it forwards when it does not.
Testing the "stops the chain" branch requires asserting a negative: that the mock next-handler was never invoked, which is just as important to cover as asserting that it was invoked in the forwarding branch, since a bug where a handler forwards even after successfully handling a request can cause the request to be processed twice.
@Test
void doesNotForwardWhenAmountWithinLimit() {
Handler next = mock(Handler.class);
LevelApprovalHandler handler = new LevelApprovalHandler("Manager", 1000);
handler.setNext(next);
handler.handle(new ExpenseRequest(500));
verifyNoInteractions(next); // must not forward once handled
}
70. Explain how to design a terminal handler that reports a clear, actionable error when no handler in the chain was able to process a request, rather than a generic or misleading failure.
A well-designed terminal (fallback) handler should capture enough context to make the failure debuggable: the request's identifying details, and ideally which handlers were consulted and why each declined, rather than a bare "not handled" exception that gives an on-call engineer nothing to work with at 3 a.m.
class NoHandlerFoundHandler extends Handler {
void handle(Request request) {
throw new NoHandlerFoundException(
"No handler processed request " + request.id() +
" of type " + request.type() + "; check chain configuration");
}
}
71. Explain the origin of the pattern's name, drawing the analogy to a real-world chain of command in an organization, and why this analogy is a useful teaching tool for the pattern's intent.
The name evokes a military or corporate chain of command, where a request (say, an approval or an escalation) is submitted to the lowest-ranking authority first, and only escalates upward to the next level if that authority lacks the power to resolve it; crucially, the requester never needs to know in advance which level of authority will ultimately act.
This analogy is useful precisely because it captures the pattern's two defining properties in an intuitive, familiar shape: sequential escalation (try the next one only if needed) and sender/receiver decoupling (the requester submits to "the process," not to a specific named individual).
72. Walk through the Gang of Four's UML class diagram for Chain of Responsibility, explaining the association between Handler and itself (the self-referential "successor" link) and how it differs from a typical composition relationship.
The GoF UML diagram shows a Handler class with a self-referential association labeled "successor," meaning a Handler holds a reference to another object of the same abstract type; ConcreteHandler subclasses inherit this association rather than each declaring their own. This is distinct from a typical whole-part composition relationship (like a car "has-a" engine) because the successor is a peer of the same type, not a structurally different part, and the relationship is specifically about behavioral delegation rather than ownership or lifecycle containment.
abstract class Handler {
protected Handler successor; // self-referential association from the UML diagram
}
73. Design a Chain of Responsibility for mapping an HTTP client library's various response status codes to typed exceptions, where each handler is responsible for one status code range.
Each handler in the chain checks whether the response's status code falls into the range it owns (client errors, server errors, redirects) and, if so, constructs and throws the appropriate typed exception; otherwise it forwards to the next handler, ending in a generic handler that wraps any unrecognized status code in a catch-all exception type.
class ClientErrorHandler extends ResponseHandler {
void handle(HttpResponse response) {
int code = response.statusCode();
if (code >= 400 && code < 500) {
throw new ClientErrorException(code, response.body());
} else if (next != null) {
next.handle(response);
}
}
}
74. Explain how to combine Chain of Responsibility with the Factory Method pattern so that the concrete handler instances populating the chain are produced by a factory rather than instantiated directly at the wiring call site.
A factory method (or a set of factory methods, one per handler family) encapsulates how each concrete handler is constructed, including any dependencies it needs injected, so the chain-assembly code deals only with the abstract Handler type returned by the factory, never with concrete constructors directly.
class HandlerFactory {
static Handler createApprovalHandler(String level, double limit) {
return new LevelApprovalHandler(level, limit); // construction details centralized here
}
}
Handler chain = HandlerFactory.createApprovalHandler("Manager", 1000);
chain.setNext(HandlerFactory.createApprovalHandler("Director", 10000));
75. Discuss whether handler instances in a Chain of Responsibility should typically be treated as singletons, and what constraints that places on how they may be implemented.
Since a chain is usually built once and reused across every request, handler instances are effectively singletons in practice, whether or not they are formally registered as such in a DI container; this reinforces the earlier point that handlers must avoid mutable per-request instance state, since a singleton instance is shared across every concurrent invocation for the lifetime of the application.
@Component // Spring singleton scope by default: this instance is reused across all requests
class AuthenticationHandler implements RequestHandler {
// no per-request mutable fields allowed here
}
76. Design a mechanism for reordering handlers in a live chain based on a feature flag, such as temporarily moving a new experimental handler earlier in the chain for a percentage of traffic.
Rather than mutating a single shared chain's order in place, build two (or more) complete chain variants at startup, one with the experimental handler in its normal position and one with it promoted earlier, and select which pre-built chain to use per request based on the feature flag evaluation, avoiding any race condition from reordering a chain while requests are actively flowing through it.
Handler standardChain = ChainFactory.buildStandard();
Handler experimentChain = ChainFactory.buildWithExperimentalHandlerPromoted();
Handler selected = featureFlags.isEnabled("promote-experimental-handler", request.userId())
? experimentChain : standardChain;
selected.handle(request);
77. Walk through the process of root-causing a production incident where requests matching a specific business rule stopped producing any downstream effect, tracing it back to a handler that began swallowing a newly introduced exception type.
The investigation typically starts from the observable symptom (requests of a certain shape produce no downstream effect), then works backward by adding request-scoped tracing across every handler boundary to identify the exact handler where the request's "trail" goes cold. Once isolated to one handler, reviewing recent changes to that handler's exception handling (often a newly added catch block introduced for an unrelated reason) usually reveals a broadened catch clause that now also swallows a case it was never intended to suppress.
// Before: caught only the expected exception
catch (SpecificValidationException ex) { return Optional.empty(); }
// After a "quick fix" broadened the catch, silently swallowing unrelated failures too
catch (Exception ex) { return Optional.empty(); } // now hides bugs instead of expected validation failures
78. Discuss the concurrency considerations of a single shared handler instance being entered by many threads simultaneously as part of a high-throughput chain, focusing on what is and is not safe to do inside handle().
Reading immutable configuration fields set once at construction is safe from any number of concurrent threads; reading and writing any instance field that changes per-request is not, without proper synchronization, and synchronizing a hot-path method to protect such state usually just turns the chain into a serialization bottleneck under load. The practical rule is to design every handler so that handle() only ever reads immutable fields and operates on data passed in through its parameters, treating any need for "shared state across the call" as a sign that state belongs in an externally injected, properly concurrent collaborator (a thread-safe cache, a metrics counter) rather than a plain instance field.
79. Explain how to write a Spring MockMvc integration test that verifies the full Servlet filter chain, including a custom authentication filter, behaves correctly for both an authenticated and an unauthenticated request.
MockMvc, when configured with @AutoConfigureMockMvc or by explicitly applying Spring Security's test support, runs requests through the real, fully assembled filter chain rather than bypassing it, letting a test assert the actual chain-level outcome, such as a 401 for a missing credential versus a 200 once a valid one is supplied, rather than testing the authentication filter's logic in isolation.
@Test
void rejectsRequestWithoutAuthHeader() throws Exception {
mockMvc.perform(get("/api/orders"))
.andExpect(status().isUnauthorized()); // exercises the real filter chain end to end
}
80. Describe how a misconfigured next reference, such as a chain factory accidentally wiring two separate sub-chains together at the wrong point, can produce a chain that silently skips an entire section of expected handlers.
If a chain-building factory assembles two logically distinct sub-chains (say, a validation sub-chain and a business-rule sub-chain) and a bug in the wiring code sets the validation sub-chain's last handler's next-reference directly to the business-rule sub-chain's second handler instead of its first, the first business-rule handler is silently skipped for every request, with no error since the chain remains fully connected, just missing one link.
// Bug: skips businessHandlers.get(0) entirely
validationHandlers.get(last).setNext(businessHandlers.get(1)); // should be get(0)
This is exactly the kind of mistake an end-to-end ordering test (as discussed earlier) is designed to catch, since a per-handler unit test in isolation would never notice a wiring bug between two handlers.
81. Explain how event bubbling in a GUI framework, where a mouse click event travels from a leaf widget up through its parent containers until something handles it, is an example of Chain of Responsibility.
An unhandled UI event, such as a click on a widget that has no specific click handler registered, is passed up to its containing parent, which may handle it or pass it further up the containment hierarchy, continuing until some ancestor handles it or the event reaches the root with no handler claiming it. The containment hierarchy itself supplies the chain's next-references, so no separate chain-construction step is needed beyond the UI's own parent-child structure.
82. Explain why a compiler's sequential passes, such as lexing, parsing, and semantic analysis, are usually better modeled as a fixed pipeline (or Template Method) rather than as a Chain of Responsibility, despite superficial similarity.
Compiler passes always run in the same fixed order, every pass always runs (barring a fatal error aborting the whole compilation), and no pass "claims" the input the way a Chain of Responsibility handler claims a request; each stage transforms its input and unconditionally hands the result to the next fixed stage. This lacks the defining Chain of Responsibility property of runtime-determined, conditional ownership among interchangeable candidates, which is why it maps more naturally to a fixed pipeline or Template Method than to Chain of Responsibility, even though both involve "passing something through a sequence of steps."
83. Explain the distinction between "Chain of Responsibility" and the more general "pipeline" architectural pattern, and clarify when a design is genuinely one versus the other.
A pipeline, in the general architectural sense, is any sequence of stages where each stage's output feeds the next stage's input, with no requirement that any stage "claims" or "owns" the data exclusively; most pipelines expect every stage to run, unconditionally, transforming the data along the way. Chain of Responsibility is a specific case where the sequence exists precisely so that one (or a few) of several candidate stages can claim exclusive responsibility and stop the sequence, which is a narrower and more specific intent than a general transformation pipeline.
A design is genuinely Chain of Responsibility, not just "a pipeline," when the central question it answers is "which one of these interchangeable candidates should handle this," rather than "what sequence of transformations should this data pass through."
84. Explain how gRPC's client-side and server-side interceptor chains implement Chain of Responsibility for RPC calls, including how an interceptor decides to proceed to the next interceptor or short-circuit the call.
A gRPC ServerInterceptor receives the call along with a ServerCallHandler representing the rest of the chain, and must invoke next.startCall(call, headers) to proceed, or can instead close the call early (for example, with an UNAUTHENTICATED status) to short-circuit it, mirroring exactly the explicit-next-call style seen in OkHttp and Netty.
class AuthServerInterceptor implements ServerInterceptor {
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
if (!isAuthenticated(headers)) {
call.close(Status.UNAUTHENTICATED, new Metadata());
return new ServerCall.Listener<>() {}; // short-circuit, never calls next.startCall
}
return next.startCall(call, headers);
}
}
85. Design a lambda-based middleware system for a small custom HTTP server, where each middleware is a BiFunction taking the request and the "rest of the chain" as a callable, without any handler class hierarchy at all.
Representing "the rest of the chain" itself as a Function<Request, Response> parameter lets each middleware be a plain lambda that decides whether to call that function (forwarding) or return its own response directly (short-circuiting), composed together by wrapping each middleware around the next at startup, entirely without a class hierarchy.
interface Middleware { Response handle(Request request, Function<Request, Response> next); }
Function<Request, Response> compose(List<Middleware> middlewares, Function<Request, Response> finalHandler) {
Function<Request, Response> chain = finalHandler;
for (int i = middlewares.size() - 1; i >= 0; i--) {
Middleware mw = middlewares.get(i);
Function<Request, Response> next = chain;
chain = req -> mw.handle(req, next);
}
return chain;
}
86. Explain how to route a request to different handler chains entirely based on a feature toggle, such as sending a subset of traffic through an experimental chain variant while the rest uses the stable chain.
Rather than branching inside individual handlers on a feature flag, which spreads the flag check across many classes and makes eventual cleanup harder, build two complete, independently testable chain variants and select between them at the single entry point based on the flag, so each chain variant remains simple and flag-free internally.
Handler chain = featureFlags.isEnabled("new-approval-flow", request.userId())
? ExperimentalApprovalChainFactory.build()
: ApprovalChainFactory.build();
chain.handle(request);
87. Explain how Apache Camel's route processors, chained together via the DSL's .process() and .to() calls, function as a Chain of Responsibility for message routing and transformation.
A Camel route defines an ordered sequence of processors and endpoints that an Exchange (Camel's message wrapper) flows through; each Processor can inspect, transform, or redirect the exchange, and routing components like choice() let a route conditionally send the exchange down one of several paths based on a predicate, closely mirroring a Chain of Responsibility's conditional forwarding, though expressed through a fluent routing DSL rather than a plain object chain.
from("direct:orders")
.choice()
.when(header("priority").isEqualTo("HIGH")).to("direct:expeditedProcessing")
.otherwise().to("direct:standardProcessing")
.end();
88. Map each classic Chain of Responsibility role, Handler, ConcreteHandler, the successor reference, and Client, onto its Apache Camel equivalent, and explain the two most important structural differences between a hand-rolled Java chain and a Camel route.
The GoF Handler role corresponds to Camel's Processor interface, with each ConcreteHandler corresponding to an individual processor implementation or DSL step such as .process() or .bean(). The Client role is whatever produces the initial Exchange, typically an inbound endpoint or a ProducerTemplate.send() call. There is, however, no direct equivalent of the successor field stored on each handler: no individual processor holds a reference to "the next one."
That absence points to the two structural differences worth naming explicitly. First, forwarding is centralized rather than decentralized: a route's internal Pipeline processor owns the ordered list and invokes each step in turn, so a plain custom Processor cannot itself decide to skip forwarding the way a hand-rolled handler can, conditional branching must instead be expressed through DSL constructs like choice() or filter(). Second, the chain's topology is described declaratively and compiled into a processor graph at route-build time, rather than wired imperatively through repeated setNext calls.
public class EnrichmentProcessor implements Processor {
public void process(Exchange exchange) {
exchange.getIn().setHeader("traceId", java.util.UUID.randomUUID().toString());
// no explicit "next" call here; the route's Pipeline invokes the next step
}
}
from("direct:orders")
.process(new EnrichmentProcessor())
.choice()
.when(header("priority").isEqualTo("HIGH")).to("direct:expedited")
.otherwise().to("direct:standard")
.end();
89. Explain the rule of thumb that a security-enforcing filter should be the very first filter to run in a servlet container's filter chain, ahead of even seemingly unrelated filters like response caching or compression, and describe a vulnerability that results from violating it.
A security filter's entire purpose is to prevent a request from reaching anything further down the chain unless it is authorized to be there, which only works if every other filter genuinely sits downstream of it. If a response-caching filter is registered with a lower order value than the security filter, it can serve a previously cached, already-authenticated response to a caller who never passed the security filter at all, because the cache lookup itself short-circuits the chain before the security filter's position is even reached.
@Bean
FilterRegistrationBean<SecurityFilter> securityFilter() {
FilterRegistrationBean<SecurityFilter> reg = new FilterRegistrationBean<>(new SecurityFilter());
reg.setOrder(Ordered.HIGHEST_PRECEDENCE); // must run before every other filter, no exceptions
return reg;
}
@Bean
FilterRegistrationBean<ResponseCachingFilter> cachingFilter() {
FilterRegistrationBean<ResponseCachingFilter> reg = new FilterRegistrationBean<>(new ResponseCachingFilter());
reg.setOrder(Ordered.HIGHEST_PRECEDENCE + 100); // deliberately after security
return reg;
}
90. Compare implementing cross-cutting concerns such as logging, metrics, and tracing as explicit links in a Chain of Responsibility versus as Spring AOP aspects woven in via dynamic proxies, and explain when each approach is preferable.
Both approaches keep cross-cutting logic out of core business methods, but they differ in where that logic sits relative to the rest of the request's routing decisions and in how visible the resulting call sequence is at read time.
| Approach | Use when | Watch out for |
|---|---|---|
| Explicit chain handler | The concern needs to interact with, or be ordered relative to, other handlers in the same request pipeline, such as a metrics handler that must run after auth but before business logic. | Adds one more class and one more wiring entry per concern, even for very small, uniform concerns. |
| Spring AOP aspect / dynamic proxy | The concern is orthogonal and applies uniformly across many unrelated methods with no need to interact with a request pipeline's own ordering. | Pointcut expressions grow hard to audit as they multiply, and proxy-based AOP silently does nothing on internal self-invocation or final methods. |
A useful heuristic: if the concern's placement relative to other steps is itself a meaningful design decision, model it as a chain link; if it is purely an unconditional wrapper with no interaction with other cross-cutting concerns, an aspect is usually less code.
91. Describe three scenarios where reaching for Chain of Responsibility is the wrong choice even though the code superficially resembles a "handler that might forward" shape, and identify the simpler alternative in each case.
Not every "try this, otherwise try that" shape genuinely benefits from the pattern's machinery. Three common false positives:
- The correct handler is always knowable in advance from a single field. Routing by an exact type or key to exactly one known implementation is a lookup, best expressed as a
Map<String, Handler>or a switch expression, not a linear scan through candidates. - Every step always runs, unconditionally, in a fixed order. That is a plain pipeline or Template Method; introducing handler objects that could in principle stop the chain, when none of them ever actually do, adds indirection with no corresponding flexibility payoff.
- The chain has grown so long that per-request latency now matters more than pluggability. A keyed dispatch table resolves in constant time; a long linear chain costs, on average, half its length in unnecessary handler checks per request.
92. Walk through refactoring a long if-else (or switch) statement that routes a request to one of several processing branches into a Chain of Responsibility, and explain what concretely improves as a result.
Each branch of the original conditional becomes its own handler class, keeping the condition that used to be an if clause as that handler's own canHandle check, and the branch's body becomes that handler's action. The branches are then linked in the same order they appeared in the original conditional, preserving existing precedence exactly.
// Before: every new request type means editing this one method
void process(Request request) {
if (request.type().equals("REFUND")) { processRefund(request); }
else if (request.type().equals("DISPUTE")) { processDispute(request); }
else if (request.type().equals("CANCELLATION")) { processCancellation(request); }
else { throw new UnsupportedOperationException("Unknown type: " + request.type()); }
}
// After: each branch is independently testable and addable without touching this method
Handler chain = new RefundHandler();
chain.setNext(new DisputeHandler());
chain.getNext().setNext(new CancellationHandler());
chain.getNext().getNext().setNext(new UnsupportedTypeHandler());
What concretely improves is that adding a new request type no longer requires editing the shared conditional method at all, each branch becomes unit-testable in isolation with a mocked next handler, and the order of checks becomes an explicit wiring decision instead of implicit statement order buried inside one growing method.
93. Describe the signs that a hand-rolled linked-list Chain of Responsibility has become unnecessary ceremony for a fixed, small set of handlers, and show how to simplify it back to a plain loop over a List<Handler> without losing behavior.
Warning signs include: the chain has never been reordered since it was written, no handler is ever loaded or registered at runtime, the full handler class hierarchy exists solely to store and forward a next reference, and every handler in the chain follows the same simple handle-and-stop contract with no handler-specific forwarding logic.
// Before: class hierarchy purely for next-reference bookkeeping
abstract class Handler { protected Handler next; abstract boolean tryHandle(Request r); }
// After: same behavior, no inheritance needed for a fixed, small, never-reordered set
List<Handler> handlers = List.of(new RefundHandler(), new DisputeHandler(), new CancellationHandler());
for (Handler h : handlers) {
if (h.tryHandle(request)) return; // identical handle-and-stop semantics, no next-reference plumbing
}
The behavior is preserved exactly because both versions try candidates in the same order and stop at the first success; what is removed is only the ceremony of a base class and next-reference wiring that was never exercised for anything beyond simple sequential iteration.
94. Walk through converting an existing class-per-handler Chain of Responsibility into a lambda or Function-based chain, and discuss what is gained and what is lost in the process.
Each stateless handler class collapses into a single lambda of type Function<Request, Optional<Response>>, and the chain-driving loop that previously called next.handle(request) instead iterates a List of these functions, stopping at the first non-empty result, exactly as in the functional-composition example discussed earlier.
// Before: a full class purely to wrap one condition and one action
class AutoApproveHandler extends Handler {
void handle(Request r) {
if (r.amount() < 100) { approve(r); } else if (next != null) { next.handle(r); }
}
}
// After: the same logic as a single lambda entry in a list
Function<Request, Optional<Response>> autoApprove =
r -> r.amount() < 100 ? Optional.of(approve(r)) : Optional.empty();
What is gained is a large reduction in boilerplate for simple, stateless steps, and composition that reads top-to-bottom as a list rather than as a scattered set of classes. What is lost is a named type for each step, which makes stack traces and profiler output less immediately meaningful, and a natural seam for dependency injection, since a lambda cannot easily declare constructor-injected collaborators the way a Spring-managed handler bean can; lambdas that need real dependencies end up capturing them awkwardly from an enclosing scope instead.
95. Explain the general pattern for letting a dependency-injection framework, Spring, CDI, Guice, or Micronaut, assemble a Chain of Responsibility's ordering automatically from bean or component metadata, and describe a common pitfall when doing so.
Across frameworks the mechanism is similar: each handler declares its own relative position through a framework-recognized ordering annotation, jakarta's @Priority for CDI, Spring's @Order or the Ordered interface, or Micronaut's @Order, and the framework's dependency-injection container collects every implementation of a shared handler interface into an already-sorted collection, which chain-assembly code simply iterates or wires in that order.
@jakarta.annotation.Priority(10)
@ApplicationScoped
class AuthenticationHandler implements RequestHandler { /* ... */ }
@jakarta.annotation.Priority(20)
@ApplicationScoped
class AuthorizationHandler implements RequestHandler { /* ... */ }
A common pitfall is that many of these containers wrap injected beans in dynamic proxies to support other cross-cutting features, such as declarative transactions or method security, and a chain-assembly step that captures a direct field reference to a handler bean at construction time, rather than always going through the container's managed reference, can end up wired to the wrong instance during testing when the container substitutes a mock or a differently-scoped proxy, producing a chain that silently behaves differently in tests than in production.
96. Design a Chain of Responsibility where the entire chain must complete within an overall deadline, and explain how you would propagate and check that deadline as the request moves from handler to handler.
Attach a deadline, an absolute Instant rather than a relative duration, to the request or an accompanying context object at the moment the chain is entered, so every handler can compute remaining time without needing to know how much time earlier handlers already consumed. Each handler checks whether the deadline has already passed before doing any real work, short-circuiting immediately with a timeout response rather than performing work that will be discarded anyway; handlers that perform blocking I/O should additionally pass the remaining budget into that call's own timeout parameter, so a single slow handler cannot silently consume the whole remaining budget on its own.
class Context { final java.time.Instant deadline; Context(java.time.Instant deadline) { this.deadline = deadline; } }
void handle(Request request, Context ctx) {
if (java.time.Instant.now().isAfter(ctx.deadline)) {
throw new DeadlineExceededException(request.id());
}
java.time.Duration remaining = java.time.Duration.between(java.time.Instant.now(), ctx.deadline);
downstreamClient.call(request, remaining); // bounded by what's actually left, not a fresh full timeout
if (next != null) next.handle(request, ctx);
}
97. Design a mechanism for a Chain of Responsibility to record, for every request, exactly which handler ultimately claimed it, or that none did, and explain how this observability data helps operate the system in production.
The handler that claims a request records its own identity, typically its class's simple name or a stable string identifier, onto a request-scoped context object at the moment it decides to handle rather than forward; a driving loop or a thin wrapping decorator can do this automatically for every handler so individual handlers do not need to remember to do it themselves.
class TracingContext { String handledBy = "NONE"; }
for (Handler h : handlers) {
if (h.tryHandle(request, ctx)) { ctx.handledBy = h.getClass().getSimpleName(); break; }
}
log.info("request {} handled by {}", request.id(), ctx.handledBy);
metrics.counter("chain.handled_by", "handler", ctx.handledBy).increment();
Aggregated over time, this data tells you whether the chain's real-world behavior still matches its design assumptions: if a chain was ordered on the assumption that most requests are claimed by the first handler but the metric shows the bulk of traffic actually falls through to a handler near the end, that is a concrete, measured signal that the chain should be reordered for both clarity and latency, not just a guess.
98. Design the request-processing pipeline of an API gateway, authentication, rate limiting, request transformation, routing to the correct backend, and response transformation, as a Chain of Responsibility, and explain how Spring Cloud Gateway's GatewayFilterChain reflects this design.
Each concern becomes its own GatewayFilter, and ordering carries real behavioral weight: rate limiting should run early enough to reject excess traffic before any deeper, more expensive processing happens, authentication must run before routing so an unauthenticated request never reaches a backend, and response transformation logically belongs to the way back through the chain, after the backend has actually responded. Spring Cloud Gateway's GatewayFilterChain.filter(exchange) mirrors the reactive WebFilterChain discussed earlier, returning a Mono<Void> that a filter can compose around to run logic both before forwarding and after the downstream response is available.
class ResponseHeaderGatewayFilter implements GatewayFilter {
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
exchange.getRequest().mutate().header("X-Gateway-In", "true").build();
return chain.filter(exchange) // forward toward routing and the backend
.then(Mono.fromRunnable(() ->
exchange.getResponse().getHeaders().add("X-Gateway-Out", "true"))); // runs on the way back
}
}
99. Diagnose a NullPointerException thrown from inside handle() at the line that calls next.handle(request), trace it to its root wiring cause, and compare two different fixes: a defensive null check versus the Null Object pattern.
Unlike the silent-drop bug from a missing else branch, this failure is loud: a handler unconditionally calls next.handle(request) without checking whether next was ever set, and the root cause is almost always a chain-building path that constructed a handler in isolation, or a factory that forgot to wire the final real handler's successor to any terminal handler at all, leaving the field at its default null value.
// Buggy: assumes next is always set
void handle(Request request) {
if (!canHandle(request)) { next.handle(request); } // NPE if next was never wired
}
A defensive null check, if (next != null) next.handle(request);, fixes the immediate symptom but must be repeated correctly in every handler, and a single omission anywhere reintroduces the bug. The Null Object pattern instead eliminates the possibility entirely by construction: give every handler's next field a non-null default pointing at a shared, singleton no-op or terminal handler, so next is genuinely never null and no handler ever needs a null check at all.
class NoOpHandler extends Handler {
static final NoOpHandler INSTANCE = new NoOpHandler();
void handle(Request request) { /* intentionally does nothing further, or logs an unhandled case */ }
}
abstract class Handler {
protected Handler next = NoOpHandler.INSTANCE; // never null by default
}
100. As a capstone, design a complete support-ticket routing system using Chain of Responsibility that combines immutable request objects, guaranteed termination, handler-claim tracing, and dependency-injection-managed ordering, and walk through how a ticket flows through it end to end.
The Ticket itself is an immutable record, carrying category, severity, and description, so no handler can mutate another handler's view of it; a separate, mutable TicketContext travels alongside it to carry cross-cutting concerns, the handler-claim trace, and the overall deadline, exactly the separation discussed for immutable requests and for deadline propagation. Each support level is registered as a dependency-injected, @Order-annotated bean, so the container assembles the escalation sequence from configuration rather than a hand-written factory, and every handler that claims the ticket records its own name into the context before returning, giving full observability into which level actually resolved any given ticket.
record Ticket(String id, String category, int severity, String description) {}
class TicketContext {
final java.time.Instant deadline;
String handledBy = "UNRESOLVED";
TicketContext(java.time.Instant deadline) { this.deadline = deadline; }
}
@Order(1)
@Component
class L1SupportHandler implements TicketHandler {
public boolean tryHandle(Ticket ticket, TicketContext ctx) {
if (java.time.Instant.now().isAfter(ctx.deadline)) return false; // respect the overall deadline
if (ticket.severity() <= 2) { resolve(ticket); ctx.handledBy = "L1Support"; return true; }
return false; // not my responsibility, forward unchanged
}
}
@Order(2)
@Component
class L2SupportHandler implements TicketHandler {
public boolean tryHandle(Ticket ticket, TicketContext ctx) {
if (ticket.severity() <= 4) { resolve(ticket); ctx.handledBy = "L2Support"; return true; }
return false;
}
}
@Order(3)
@Component
class L3SupportHandler implements TicketHandler {
public boolean tryHandle(Ticket ticket, TicketContext ctx) {
resolve(ticket); ctx.handledBy = "L3Support"; return true; // always resolves what reaches it
}
}
@Order(Ordered.LOWEST_PRECEDENCE)
@Component
class HumanReviewFallbackHandler implements TicketHandler {
public boolean tryHandle(Ticket ticket, TicketContext ctx) {
queueForHumanReview(ticket); // guaranteed terminal handler, never a silent drop
ctx.handledBy = "HumanReview";
return true;
}
}
@Service
class TicketRouter {
private final List<TicketHandler> handlers; // injected in @Order sequence
TicketRouter(List<TicketHandler> handlers) { this.handlers = handlers; }
TicketContext route(Ticket ticket) {
TicketContext ctx = new TicketContext(java.time.Instant.now().plusSeconds(30));
for (TicketHandler h : handlers) {
if (h.tryHandle(ticket, ctx)) break;
}
log.info("ticket {} resolved by {}", ticket.id(), ctx.handledBy);
return ctx;
}
}
A low-severity ticket is claimed at L1 and never travels further; a high-severity ticket skips L1 and L2 in sequence and is claimed at L3; and if the deadline is already exceeded by the time a handler is reached, the ticket falls all the way through to the human-review fallback rather than silently vanishing, exactly the guaranteed-termination property from the earlier discussion of unterminated chains, now composed with DI-managed ordering and end-to-end tracing.
Post a Comment
Add