Fundamentals 32 min read

Why I Won’t Take the Blame: Mastering the Chain of Responsibility Pattern

This article explains the Chain of Responsibility design pattern, shows classic Java implementations, demonstrates its use in Spring Security, provides real‑world examples such as order processing, logging pipelines, and approval workflows, and offers advanced functional and asynchronous techniques along with performance tips and common pitfalls.

Cloud Architecture
Cloud Architecture
Cloud Architecture
Why I Won’t Take the Blame: Mastering the Chain of Responsibility Pattern

Introduction

In the workplace, "passing the buck" is an art. In programming, the same idea becomes the elegant Chain of Responsibility pattern , where a request travels along a chain and each handler can either process it or pass it to the next.

Core Concepts

The pattern consists of three main roles:

Handler (abstract handler) : defines the handling interface and holds a reference to the next handler.

ConcreteHandler : implements the actual processing logic and decides whether to return a result or delegate to the next handler.

Client : assembles the chain and initiates the request.

The essence is to decouple the request sender from the receiver, allowing multiple objects a chance to handle the request.

Classic Implementations

Traditional (abstract class + inheritance)

public abstract class Handler {
    protected Handler nextHandler;
    public Handler setNext(Handler nextHandler) { this.nextHandler = nextHandler; return nextHandler; }
    public final Object handle(Request request) {
        Object result = doHandle(request);
        if (result != null) return result;
        if (nextHandler != null) return nextHandler.handle(request);
        return handleDefault(request);
    }
    protected abstract Object doHandle(Request request);
    protected Object handleDefault(Request request) { return new Result(false, "Request cannot be handled"); }
}

public class HandlerA extends Handler { /* implementation */ }
public class HandlerB extends Handler { /* implementation */ }
public class HandlerC extends Handler { /* implementation */ }

public class Client {
    public static void main(String[] args) {
        Handler handlerA = new HandlerA();
        Handler handlerB = new HandlerB();
        Handler handlerC = new HandlerC();
        handlerA.setNext(handlerB).setNext(handlerC);
        // create requests of type A, B, C, D and invoke handlerA.handle(request)
    }
}

Interface‑based (more flexible)

public interface RequestHandler {
    Result handle(Request request, RequestChain chain);
}

public class RequestChain {
    private final List<RequestHandler> handlers;
    private int currentIndex = 0;
    public RequestChain(List<RequestHandler> handlers) { this.handlers = handlers; }
    public Result proceed(Request request) { /* invoke next handler or return default */ }
    public Result execute(Request request) { currentIndex = 0; return proceed(request); }
}

Functional (lambda style)

@FunctionalInterface
public interface FunctionHandler<T, R> {
    R apply(T input, Function<T, R> next);
    default FunctionHandler<T, R> andThen(FunctionHandler<T, R> after) {
        return (input, next) -> this.apply(input, t -> after.apply(t, next));
    }
}

Chain of Responsibility in Spring Security

Spring Security implements the pattern through a Security Filter Chain . The request passes through a series of filters such as ChannelProcessingFilter, SecurityContextPersistenceFilter, UsernamePasswordAuthenticationFilter, and finally reaches the controller. Custom filters (e.g., a JWT authentication filter) can be inserted before or after any built‑in filter.

Real‑World Scenarios

Scenario 1 – E‑commerce Order Processing Pipeline

Steps: stock check → coupon validation → payment pre‑processing → risk control → order creation. Each step is a OrderHandler implementation (e.g., StockCheckHandler, CouponValidationHandler, PaymentPreHandler, RiskControlHandler, OrderCreateHandler). The OrderService wires all handlers via Spring’s @Order annotation and executes the chain.

Scenario 2 – Logging Pipeline

Handlers include LevelFilterHandler (drops low‑level logs), SensitiveDataFilterHandler (redacts passwords, tokens), FormatHandler (adds timestamp and level), AsyncAppenderHandler (writes to console, file, DB asynchronously), and AlertHandler (alerts on ERROR/FATAL). The Logger class builds a LogChain and exposes debug/info/warn/error methods.

Scenario 3 – Multi‑Level Approval Workflow

Handlers such as DirectManagerHandler, HRApprovalHandler, FinanceApprovalHandler, and DirectorApprovalHandler form a dynamic ApprovalChain. The chain is selected based on request type (LEAVE, EXPENSE, PURCHASE) and can be cached per type.

Advanced Usage

Functional Chain with Java Streams

ChainBuilder<String, String> builder = new ChainBuilder<>();
String result = builder
    .addStageIf(s -> s.length() > 10, s -> { System.out.println("[1] Truncate"); return s.substring(0,10); })
    .addStage(s -> { System.out.println("[2] Uppercase"); return s.toUpperCase(); })
    .addStageIf(s -> !s.endsWith("!"), s -> { System.out.println("[3] Add exclamation"); return s + "!"; })
    .execute("  hello world  ", s -> { System.out.println("[Terminal] " + s); return s; });
System.out.println("Result: " + result);

Asynchronous Chain with CompletableFuture

AsyncChain<OrderResult> chain = new AsyncChain<>(() -> {
    System.out.println("[Init] Create order context");
    return new OrderResult(true, "Initial", "ORD001", BigDecimal.ZERO);
});
OrderResult result = chain
    .thenApplyAsync(r -> { System.out.println("[Async 1] Stock check"); sleep(100); return r; })
    .thenApplyAsync(r -> { System.out.println("[Async 2] Payment validation"); sleep(100); return r; })
    .thenApplyIfAsync(r -> r.isSuccess(), r -> { System.out.println("[Async 3] Risk review"); sleep(100); return new OrderResult(true, "Approved", r.getOrderId(), BigDecimal.valueOf(100)); })
    .exceptionally(e -> { System.err.println("Error: " + e.getMessage()); return new OrderResult(false, "Failed", null, BigDecimal.ZERO); })
    .get(5, TimeUnit.SECONDS);
System.out.println("Final result: " + result);

Performance Optimizations & Best Practices

Avoid overly long chains : group related handlers (validation, business, post‑processing) to keep the chain manageable.

Short‑circuiting : let a handler return a failure result early to stop further processing.

Cache results : use a cache (e.g., Guava) for idempotent operations and store successful outcomes.

Monitoring & logging : wrap handlers with metrics (success/failure counters, duration timers) and emit warnings for long‑running handlers.

Common Pitfalls & Solutions

Forgot to pass to the next handler

// Wrong – returns without delegating
public Result handle(Request request, RequestChain chain) { System.out.println("Processing..."); return Result.success(); }
// Correct – delegate to the chain
public Result handle(Request request, RequestChain chain) { System.out.println("Processing..."); return chain.proceed(request); }

Circular references

// Wrong – creates a loop a → b → c → a
// Correct – ensure the last handler has no next reference

Thread‑safety issues

// Wrong – mutable shared state (e.g., currentIndex) without synchronization
// Correct – keep handlers stateless or use ThreadLocal where needed

Improper exception handling

// Wrong – swallow all exceptions and return success
// Correct – log business exceptions, return a failure result, and re‑throw system exceptions

Conclusion

The Chain of Responsibility pattern is a powerful tool for scenarios that require multi‑step, pluggable, sequential processing. Its key benefits are decoupling, flexibility, single‑responsibility, and extensibility. It fits well for approval workflows, request filtering, logging pipelines, and interceptor chains, while being mindful of chain length, short‑circuiting, caching, monitoring, thread safety, and proper error handling.

References

Chain of Responsibility – Wikipedia

Spring Security Filter Chain documentation

Java Servlet Filter specification

Design Patterns – Chain of Responsibility chapter

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Chain of ResponsibilityJavaMiddlewareDesign Patternspring security
Cloud Architecture
Written by

Cloud Architecture

Focuses on cloud‑native and distributed architecture engineering, sharing practical solutions and lessons learned. Covers microservice governance, Kubernetes, observability, and stability engineering to help your systems run stable, fast, and cost‑effectively.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.