Skip to Content
Course content

292: The Chain of Responsibility Pattern

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this pattern play out in almost every mid-sized enterprise project I've touched: a developer starts with a simple piece of logic to handle a request, and six months later, they're staring at a 500-line method filled with nested if-else blocks. It usually happens when you're building something like a request processing pipeline—think of a system that needs to authenticate a user, check their permissions, validate the input, and then finally log the action.

The "Giant If-Else" Trap

When you first build a request pipeline, the naive approach is just to put everything in one place. You create a RequestProcessor class and a single method that handles the logic. It looks something like this:

public void processRequest(Request request) {
    if (request.isMissingAuthToken()) {
        throw new AuthException("No token provided");
    }
    if (!authService.isValid(request.getToken())) {
        throw new AuthException("Invalid token");
    }
    if (rateLimiter.isExceeded(request.getIp())) {
        throw new RateLimitException("Too many requests");
    }
    if (!validator.isValid(request.getBody())) {
        throw new ValidationException("Bad payload");
    }
    // Finally, do the actual work...
    doWork(request);
}

At first glance, this is actually quite readable. It's linear. You can see exactly what happens and in what order. But here is where the rot sets in. What happens when the product manager asks you to add a "Maintenance Mode" check? Or a "Premium User" bypass for the rate limiter? Or a specialized validation step for only certain types of requests?

Suddenly, your processRequest method is an absolute monster. You're modifying the same file every single time a new requirement drops, which is a nightmare for version control and a violation of the Open/Closed Principle. You're forcing one class to know everything about authentication, rate limiting, and validation. That's a recipe for a "God Class" that everyone is afraid to touch.

Decoupling with the Chain of Responsibility

The better way—and the core of the Chain of Responsibility pattern—is to stop thinking of this as one big process and start thinking of it as a series of independent "handlers." Each handler has one job: either process the request or pass it to the next person in line.

I like to think of it like a corporate approval process. You send a request to your manager; if they can't approve it, they pass it to the director; if the director can't, it goes to the VP. You don't need to know who the VP is; you just need to know who your manager is.

In Java, we implement this by defining a common interface for our handlers and giving each handler a reference to the next one in the chain:

public abstract class Handler {
    protected Handler next;

    public void setNext(Handler next) {
        this.next = next;
    }

    public abstract void handle(Request request);

    protected void passToNext(Request request) {
        if (next != null) {
            next.handle(request);
        }
    }
}

Now, your specific logic is encapsulated in small, focused classes. An AuthHandler only cares about tokens. A RateLimitHandler only cares about IPs. If the AuthHandler sees the token is valid, it simply calls passToNext(request). If it sees the token is missing, it throws the exception and the chain stops immediately.

Trading Control for Flexibility

Now, I'll be honest with you: this approach has a cost. You've traded a simple, linear method for a more complex object graph. If you're debugging a request and you don't know how the chain was assembled, it can feel like you're chasing a ghost through your codebase. You can't just "jump to definition" to see the whole flow; you have to figure out which handlers were linked together at runtime.

However, the trade-off is almost always worth it as the project grows. You can now reorder the chain, add new handlers, or remove old ones without touching the code of the other handlers. You can even build different chains for different environments—for example, a "DevelopmentChain" that skips the rate limiter but keeps the authentication. You've moved the configuration of the process away from the implementation of the process, and that is where the real power lies.




📋 Practical Task

Implementing a Multi-Tier Customer Support Router

You are building a customer support ticket routing system. Tickets come in with a "severity" level (LOW, MEDIUM, HIGH). Depending on the severity, different tiers of support should handle the ticket. If a tier cannot handle the ticket (because it's too severe), it should be passed to the next tier.

Your Task:

  • Create a Ticket class with a severity field (using an Enum).
  • Implement a SupportHandler abstract class that follows the Chain of Responsibility pattern.
  • Create three concrete handlers: JuniorSupport (handles LOW), SeniorSupport (handles MEDIUM), and ManagerSupport (handles HIGH).
  • If a ticket is passed to a handler that cannot handle its severity, the handler should pass it to the next person in the chain.
  • If a ticket reaches the end of the chain without being handled, print a message stating the ticket could not be processed.
  • In your Main class, assemble the chain (Junior → Senior → Manager) and test it with tickets of varying severities.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.