June 17, 2026 in patterns5 minutes
A intermediate-level guide to Chain of Responsibility: before-and-after java code and diagrams for a CS student.
When building systems that handle different types of requests—like support tickets or error logs—it often starts with a simple if-else block. However, as the system grows to accommodate more categories, that single method becomes a “God Method.” It knows too much about every possible request type, and every time you add a new category, you have to modify this same central logic.
This is a classic code smell: Switch Statements (or large if-else ladders). By using these conditionals to decide which logic to run, you violate the Open/Closed Principle—the idea that software entities should be open for extension but closed for modification. Instead of adding new functionality by adding new classes, you are forced to keep hacking away at an existing, increasingly fragile method.
public class SupportTicketProcessor {
public void process(String ticketType) {
if ("TECHNICAL".equals(ticketType)) {
System.out.println("Routing to Technical Support...");
} else if ("BILLING".equals(ticketType)) {
System.out.println("Routing to Billing Department...");
} else if ("SALES".equals(ticketType)) {
System.out.println("Routing to Sales Team...");
} else {
System.out.println("Unknown ticket type. Routing to General Agent.");
}
}
}In this version, the SupportTicketProcessor is tightly coupled to every specific department. If a “Legal” department is added tomorrow, this class must change. The control flow is trapped inside a single monolithic block:
flowchart TD
Start([process]) --> C1{TECHNICAL?}
C1 -- yes --> R1[Routing to Technical Support]
C1 -- no --> C2{BILLING?}
C2 -- yes --> R2[Routing to Billing Department]
C2 -- no --> C3{SALES?}
C3 -- yes --> R3[Routing to Sales Team]
C3 -- no --> R4[General Agent]
Imagine a physical mailroom in a large office. When a letter arrives, the receptionist doesn’t decide if it belongs to the CEO or the Accountant; they simply look at the label and pass it to the next person in line who is qualified to handle that specific type of document. If no one in the chain can process it, it eventually hits a general clerk who handles everything else.
This “passing the baton” approach allows each person (or object) to focus on one specific task. They either handle the request or pass it along to the next link in the chain.
To implement this in code, we use a structural design pattern called Chain of Responsibility. Instead of one giant method making all the decisions, we create a series of “Handler” objects.
Each handler shares a common type (an abstract class or interface) so they can be linked together. Each link holds a reference to the next handler in the chain. When a request comes in, it is handed to the first object; if that object cannot handle it, it delegates the work to its next neighbor.
classDiagram
class Handler {
<<abstract>>
#Handler next
+setNext(Handler next) void
+handle(String ticketType) void*
}
class TechnicalHandler {
+handle(String ticketType) void
}
class BillingHandler {
+handle(String ticketType) void
}
class SalesHandler {
+handle(String ticketType) void
}
class GeneralHandler {
+handle(String ticketType) void
}
Handler <|-- TechnicalHandler
Handler <|-- BillingHandler
Handler <|-- SalesHandler
Handler <|-- GeneralHandler
SupportTicketProcessor --> Handler : chain
By applying the Replace Conditional with Polymorphism refactoring, we move the decision-making logic out of a single method and into specialized classes. This makes the objects less dependent on each other (loose coupling) because the SupportTicketProcessor no longer needs to know about “Technical” or “Billing” specifically; it only needs to know how to trigger the start of the chain.
abstract class Handler {
protected Handler next;
public void setNext(Handler next) {
this.next = next;
}
public abstract void handle(String ticketType);
}
class TechnicalHandler extends Handler {
public void handle(String ticketType) {
if ("TECHNICAL".equals(ticketType)) {
System.out.println("Routing to Technical Support...");
} else if (next != null) {
next.handle(ticketType);
}
}
}
class BillingHandler extends Handler {
public void handle(String ticketType) {
if ("BILLING".equals(ticketType)) {
System.out.println("Routing to Billing Department...");
} else if (next != null) {
next.handle(ticketType);
}
}
}
class SalesHandler extends Handler {
public void handle(String ticketType) {
if ("SALES".equals(ticketType)) {
System.out.println("Routing to Sales Team...");
} else if (next != null) {
next.handle(ticketType);
}
}
}
class GeneralHandler extends Handler {
public void handle(String ticketType) {
System.out.println("Unknown ticket type. Routing to General Agent.");
}
}
public class SupportTicketProcessor {
private final Handler chain;
public SupportTicketProcessor(Handler firstHandler) {
this.chain = firstHandler;
}
public void process(String ticketType) {
chain.handle(ticketType);
}
}Notice that each concrete handler like TechnicalHandler now has a clear responsibility: check if the ticket matches its expertise, and if not, call next.handle(ticketType). This allows us to assemble different chains at runtime. We could even change the order of handlers without changing the logic inside them!
In this sequence diagram, we see how a “BILLING” request skips through the chain until it finds its match:
sequenceDiagram
participant P as SupportTicketProcessor
participant T as TechnicalHandler
participant B as BillingHandler
participant G as GeneralHandler
P ->> T: handle("BILLING")
T ->> B: handle("BILLING")
B ->> B: [Matches BILLING]
Note right of B: Routing to Billing Department...
The Chain of Responsibility is powerful when you have multiple objects capable of handling a request, and the specific handler isn’t known until runtime. It excels at keeping your code clean as requirements grow.
However, it has trade-offs:
if statement.GeneralHandler at the end of your chain, some requests might simply disappear into a void without being processed. Use it when the number of handlers is manageable and the logic for “passing along” is straightforward.if-else chains with a series of specialized handler objects.Assembling the chain and processing a request
Handler tech = new TechnicalHandler();
Handler billing = new BillingHandler();
Handler sales = new SalesHandler();
Handler general = new GeneralHandler();
tech.setNext(billing);
billing.setNext(sales);
sales.setNext(general);
SupportTicketProcessor processor = new SupportTicketProcessor(tech);
processor.process("BILLING");