The Chain of Responsibility is a behavioral design pattern that lets you pass requests along a chain of handlers. Upon receiving a request, each handler decides either to process the request or to pass it to the next handler in the chain.
Imagine a line of people passing buckets of water to put out a fire. Not every person needs to act on every bucket; some might be specialized for certain tasks. The Chain of Responsibility works similarly: a request (the bucket) is passed down a line of handler objects, and each has a chance to deal with it.
This pattern decouples the sender of a request from its receivers, promoting cleaner code and adherence to the Single Responsibility Principle.
The Problem: The Monolithic if-else Beast
Let’s consider a common scenario: processing a new customer order. A single method might be responsible for everything:
- Authenticating the user.
- Authorizing if the user can make a purchase.
- Checking if the product is in stock.
- Verifying the user has enough funds.
- Processing the payment.
- Creating the order.
- Sending a notification.
This often leads to a massive, unmaintainable method filled with nested if-else statements.
public class OrderProcessor {
public void process(User user, Product product, int quantity) {
// 1. Authentication
if (!user.isAuthenticated()) {
throw new SecurityException("User is not authenticated.");
}
// 2. Authorization
if (!user.canCreateOrders()) {
throw new SecurityException("User does not have permission to create orders.");
}
// 3. Stock Check
if (product.getStock() < quantity) {
throw new IllegalStateException("Insufficient stock for product: " + product.getName());
}
// 4. Balance Check
double totalCost = product.getPrice() * quantity;
if (user.getBalance() < totalCost) {
throw new IllegalStateException("Insufficient balance.");
}
// 5. Payment Processing
System.out.println("Processing payment...");
user.withdraw(totalCost);
System.out.println("Payment successful.");
// 6. Order Creation
System.out.println("Creating order...");
Order newOrder = new Order(user.getId(), product.getId(), quantity);
System.out.println("Order " + newOrder.getId() + " created successfully.");
// 7. Notification
System.out.println("Sending confirmation email to " + user.getEmail());
}
}
[!NOTE] This
OrderProcessorclass violates core software design principles:
- Single Responsibility Principle (SRP): The class does too much. It handles authentication, inventory, billing, and notifications.
- Open/Closed Principle (OCP): To add a new step (e.g., fraud detection), you must modify this already complex class, increasing the risk of introducing bugs.
The Solution: Building a Chain of Handlers
The Chain of Responsibility pattern refactors this monolith into a flexible pipeline of handler objects. Each handler is responsible for a single task.
Here’s a visual representation of our order processing pipeline:
graph TD;
A[Client Request] --> B(Authentication Handler);
B --> C(Authorization Handler);
C --> D(Stock Check Handler);
D --> E(Balance Handler);
E --> F(Payment Handler);
F --> G(Order Creation Handler);
G --> H(Notification Handler);
H --> I[✅ Process Complete];
Core Components
- Handler Interface: The common interface for all handlers. It declares a method for processing the request and another for setting the next handler in the chain.
- Concrete Handlers: Implementations of the Handler interface, each performing a specific task. After its work is done, it passes the request to the next handler.
- Client: The client creates the chain of handlers and initiates the request by calling the
handle()method on the first handler.
Let’s visualize the class structure for our solution.
classDiagram
direction LR
class Client
class OrderRequest
class IOrderHandler {
<<interface>>
+setNext(IOrderHandler handler)
+handle(OrderRequest request)
}
class AbstractOrderHandler {
#IOrderHandler nextHandler
+setNext(IOrderHandler handler)
+handle(OrderRequest request)
}
class AuthenticationHandler
class AuthorizationHandler
class StockCheckHandler
IOrderHandler <|.. AbstractOrderHandler
AbstractOrderHandler <|-- AuthenticationHandler
AbstractOrderHandler <|-- AuthorizationHandler
AbstractOrderHandler <|-- StockCheckHandler
Client ..> IOrderHandler : initiates
Client ..> OrderRequest : creates
IOrderHandler "1" --o "0..1" IOrderHandler : next
IOrderHandler ..> OrderRequest : handles
Step-by-Step Implementation in Java
First, let’s organize our files. We’ll create a dedicated folder for our handlers.
src/
└── main/
└── java/
└── com/
└── example/
├── model/
│ ├── User.java
│ ├── Product.java
│ └── OrderRequest.java
├── handlers/
│ ├── IOrderHandler.java
│ ├── AbstractOrderHandler.java
│ ├── AuthenticationHandler.java
│ ├── AuthorizationHandler.java
│ ├── StockCheckHandler.java
│ ├── BalanceHandler.java
│ ├── PaymentHandler.java
│ ├── OrderCreationHandler.java
│ └── NotificationHandler.java
├── OrderChainBuilder.java
└── Main.java
1. The Request Object
Instead of passing multiple parameters, we’ll encapsulate all request data into a single object. This makes the handler’s method signature clean and easily extensible.
// model/OrderRequest.java
public class OrderRequest {
private User user;
private Product product;
private int quantity;
// Getters and Setters...
}
2. The Handler Interface & Base Class
The interface defines the contract, and an abstract base class provides boilerplate code for managing the chain linkage.
// handlers/IOrderHandler.java
public interface IOrderHandler {
void setNext(IOrderHandler handler);
void handle(OrderRequest request);
}
// handlers/AbstractOrderHandler.java
public abstract class AbstractOrderHandler implements IOrderHandler {
protected IOrderHandler nextHandler;
@Override
public void setNext(IOrderHandler handler) {
this.nextHandler = handler;
}
@Override
public void handle(OrderRequest request) {
// If there's a next handler, pass the request along.
if (nextHandler != null) {
nextHandler.handle(request);
}
}
}
3. Concrete Handlers
Each handler extends AbstractOrderHandler and implements its specific logic. It calls super.handle(request) to pass control to the next handler in the chain.
// handlers/AuthenticationHandler.java
public class AuthenticationHandler extends AbstractOrderHandler {
@Override
public void handle(OrderRequest request) {
if (!request.getUser().isAuthenticated()) {
throw new SecurityException("User is not authenticated.");
}
System.out.println("✅ User authenticated.");
super.handle(request); // Pass to the next handler
}
}
// handlers/StockCheckHandler.java
public class StockCheckHandler extends AbstractOrderHandler {
@Override
public void handle(OrderRequest request) {
if (request.getProduct().getStock() < request.getQuantity()) {
throw new IllegalStateException("Insufficient stock.");
}
System.out.println("✅ Product is in stock.");
super.handle(request); // Pass to the next handler
}
}
// ... and so on for AuthorizationHandler, BalanceHandler, etc.
4. Building and Using the Chain
A builder class is a clean way to assemble the chain. The client then uses this builder to get the starting point of the chain.
// OrderChainBuilder.java
public class OrderChainBuilder {
public IOrderHandler build() {
// 1. Create all handlers
IOrderHandler authHandler = new AuthenticationHandler();
IOrderHandler authorizeHandler = new AuthorizationHandler();
IOrderHandler stockHandler = new StockCheckHandler();
IOrderHandler balanceHandler = new BalanceHandler();
IOrderHandler paymentHandler = new PaymentHandler();
IOrderHandler orderHandler = new OrderCreationHandler();
IOrderHandler notificationHandler = new NotificationHandler();
// 2. Link them together to form the chain
authHandler.setNext(authorizeHandler);
authorizeHandler.setNext(stockHandler);
stockHandler.setNext(balanceHandler);
balanceHandler.setNext(paymentHandler);
paymentHandler.setNext(orderHandler);
orderHandler.setNext(notificationHandler);
// 3. Return the first handler in the chain
return authHandler;
}
}
The client code is now incredibly simple and declarative.
// Main.java (Client Code)
public class Main {
public static void main(String[] args) {
- // Old, monolithic approach
- OrderProcessor processor = new OrderProcessor();
- processor.process(user, product, quantity);
+ // New, Chain of Responsibility approach
+ OrderChainBuilder chainBuilder = new OrderChainBuilder();
+ IOrderHandler orderPipeline = chainBuilder.build();
+
+ OrderRequest request = new OrderRequest(user, product, quantity);
+ orderPipeline.handle(request);
+ System.out.println("\n🎉 Order processing completed successfully!");
}
}
Chain Variations
The example above demonstrates a Full Chain Execution, where every handler in the chain must process the request. However, another common variation exists.
Deep Dive: Full Chain vs. Short-Circuiting Chain
* **Full Chain (Our Example):** The request passes through every handler. This is ideal for processes like validation or enrichment, where each step is mandatory and builds upon the last. The `handle` method in each concrete handler always calls the next handler. * **Short-Circuiting Chain:** The chain is broken as soon as one handler fully processes the request. This is useful when multiple handlers *could* process a request, but only one should. For example, routing a support ticket based on its category (`BillingHandler`, `TechnicalSupportHandler`, `GeneralInquiryHandler`). To implement this, a handler would simply **not** call `super.handle(request)` if it successfully processed the request. ```java // Example of a short-circuiting handler public class CachingHandler extends AbstractOrderHandler { @Override public void handle(OrderRequest request) { if (cache.contains(request.getProductId())) { System.out.println("✅ Result found in cache. No further processing needed."); // Do NOT call super.handle(request) } else { // Not in cache, pass to the next handler super.handle(request); } } } ```Best Practices & Potential Pitfalls
[!TIP] Use a Builder: As demonstrated, a dedicated Builder class for assembling the chain keeps the client code clean and separates the chain’s construction from its usage.
[!TIP] Immutable Requests: Make the request object (e.g.,
OrderRequest) immutable if possible. If handlers need to modify state, have them create a new, modified request object to pass down the chain. This prevents unexpected side effects.
[!WARNING] Unhandled Requests: Ensure that a request doesn’t fall off the end of the chain without being handled if that’s not the desired behavior. You can have a final, default handler that logs or throws an error for unhandled requests.
Circular Chains: Be careful not to create a loop in your chain (e.g., Handler A points to B, and B points back to A). This will result in a
StackOverflowError. A builder can help prevent this by enforcing a linear construction process.
Conclusion
The Chain of Responsibility pattern is a powerful tool for transforming complex, conditional logic into a clean, decoupled, and extensible system. By breaking down a large process into a series of independent, linkable handlers, you create code that is easier to understand, maintain, and test, fully embracing the Single Responsibility and Open/Closed principles.