Chain of Responsibility Pattern: Building a Cash Dispenser (UML & Code)
1. Introduction to the Chain of Responsibility Pattern
The Chain of Responsibility is a behavioral design pattern that allows an incoming request to pass along a sequential chain of potential handlers until one of them handles the request or the end of the chain is reached. Instead of coupling the sender directly to a specific receiver, multiple objects are given a chance to process the request, decoupling the sender from the exact execution context.
Real-Life Analogy
The ATM Money Dispenser Analogy: When you visit an ATM to withdraw cash, the machine does not pull money from a single monolithic pile. Instead, it maintains separate physical bins or handlers for different currency denominations (e.g., 500, 100). When you request 1000 handler. If it has enough notes, it fulfills part or all of the request; if an amount remains, it delegates the remainder down the chain to the $500 handler, and so on.
2. Core Mental Model & The Linked List Metaphor
At first glance, the Chain of Responsibility pattern evokes memories of a Linked List:
- Objects are linked together via references (
nextpointers). - Requests travel sequentially through the chain.
However, unlike a traditional data structure linked list that stores homogenous data elements, the Chain of Responsibility links behavioral handler objects. Each handler in the chain possesses its own discrete business logic and operational constraints.
3. Naive Approach vs. Design Pattern Solution
The Naive If-Else Approach
A developer might initially attempt to solve multi-tier request processing using giant if-else or switch-case statements inside a single monolithic class:
// Naive / Anti-pattern approach
public void dispenseMoney(int amount) {
if (amount >= 1000) {
// handle 1000 notes
} else if (amount >= 500) {
// handle 500 notes
} else if (amount >= 200) {
// handle 200 notes
} else {
// handle 100 notes
}
}
Why the Naive Approach Fails
- Violation of Open-Closed Principle (OCP): Introducing a new denomination (e.g., a $50 note) forces you to modify existing, tested code.
- Violation of Single Responsibility Principle (SRP): A single class becomes bloated with business logic for every single currency denomination.
The Design Pattern Solution
By using the Chain of Responsibility pattern, every denomination becomes an independent class inheriting from a shared abstract handler. The next pointer is managed dynamically at runtime, ensuring complete decoupling.
4. Architectural UML Design
The architecture relies on an abstract base class that maintains a protected reference to the next handler in the sequence. Concrete handlers extend this base class and implement their specific dispensing logic.
5. Complete Java Code Implementation
Below is the complete, production-grade implementation modeling the ATM Cash Dispenser system in Java.
Abstract Base Handler (MoneyHandler.java)
public abstract class MoneyHandler {
protected MoneyHandler nextHandler;
public void setNextHandler(MoneyHandler nextHandler) {
this.nextHandler = nextHandler;
}
public abstract void dispense(int amount);
}
Concrete Handlers (ThousandHandler.java, FiveHundredHandler.java, etc.)
public class ThousandHandler extends MoneyHandler {
private int numberOfNotes;
public ThousandHandler(int numberOfNotes) {
this.numberOfNotes = numberOfNotes;
}
@Override
public void dispense(int amount) {
int notesNeeded = amount / 1000;
int remainingAmount = amount;
if (notesNeeded > 0) {
int notesToUse = Math.min(notesNeeded, numberOfNotes);
numberOfNotes -= notesToUse;
remainingAmount -= (notesToUse * 1000);
System.out.println("Dispensing " + notesToUse + " x 1000 notes.");
}
if (remainingAmount > 0) {
if (nextHandler != null) {
nextHandler.dispense(remainingAmount);
} else {
System.out.println("Error: Insufficient funds or unsupported denomination. Remaining: " + remainingAmount);
}
}
}
}
public class FiveHundredHandler extends MoneyHandler {
private int numberOfNotes;
public FiveHundredHandler(int numberOfNotes) {
this.numberOfNotes = numberOfNotes;
}
@Override
public void dispense(int amount) {
int notesNeeded = amount / 500;
int remainingAmount = amount;
if (notesNeeded > 0) {
int notesToUse = Math.min(notesNeeded, numberOfNotes);
numberOfNotes -= notesToUse;
remainingAmount -= (notesToUse * 500);
System.out.println("Dispensing " + notesToUse + " x 500 notes.");
}
if (remainingAmount > 0) {
if (nextHandler != null) {
nextHandler.dispense(remainingAmount);
} else {
System.out.println("Error: Insufficient funds or unsupported denomination. Remaining: " + remainingAmount);
}
}
}
}
public class TwoHundredHandler extends MoneyHandler {
private int numberOfNotes;
public TwoHundredHandler(int numberOfNotes) {
this.numberOfNotes = numberOfNotes;
}
@Override
public void dispense(int amount) {
int notesNeeded = amount / 200;
int remainingAmount = amount;
if (notesNeeded > 0) {
int notesToUse = Math.min(notesNeeded, numberOfNotes);
numberOfNotes -= notesToUse;
remainingAmount -= (notesToUse * 200);
System.out.println("Dispensing " + notesToUse + " x 200 notes.");
}
if (remainingAmount > 0) {
if (nextHandler != null) {
nextHandler.dispense(remainingAmount);
} else {
System.out.println("Error: Insufficient funds or unsupported denomination. Remaining: " + remainingAmount);
}
}
}
}
public class HundredHandler extends MoneyHandler {
private int numberOfNotes;
public HundredHandler(int numberOfNotes) {
this.numberOfNotes = numberOfNotes;
}
@Override
public void dispense(int amount) {
int notesNeeded = amount / 100;
int remainingAmount = amount;
if (notesNeeded > 0) {
int notesToUse = Math.min(notesNeeded, numberOfNotes);
numberOfNotes -= notesToUse;
remainingAmount -= (notesToUse * 100);
System.out.println("Dispensing " + notesToUse + " x 100 notes.");
}
if (remainingAmount > 0) {
if (nextHandler != null) {
nextHandler.dispense(remainingAmount);
} else {
System.out.println("Error: Insufficient funds or unsupported denomination. Remaining: " + remainingAmount);
}
}
}
}
Client Execution Driver (ATMClient.java)
public class ATMClient {
public static void main(String[] args) {
// Initialize handlers with specific note inventories
MoneyHandler h1000 = new ThousandHandler(3);
MoneyHandler h500 = new FiveHundredHandler(5);
MoneyHandler h200 = new TwoHundredHandler(10);
MoneyHandler h100 = new HundredHandler(20);
// Build the chain of responsibility: 1000 -> 500 -> 200 -> 100
h1000.setNextHandler(h500);
h500.setNextHandler(h200);
h200.setNextHandler(h100);
System.out.println("--- Withdrawing 4300 ---");
h1000.dispense(4300);
}
}
6. Step-by-Step Execution Flow Walkthrough
When h1000.dispense(4300) is invoked, the execution flows sequentially:
7. Comparison: Linked List vs. Chain of Responsibility
8. Alternative Design Considerations & Trade-offs
- Why not use a monolithic
if-elseblock? Whileif-elsestatements work for simple scripts, they cause severe maintenance bottlenecks as business rules grow. The Chain of Responsibility pattern encapsulates each rule into its own class, honoring the Single Responsibility Principle. - Dynamic Configuration: Because handlers link via abstract interfaces (
MoneyHandler), you can alter, insert, or remove handlers at runtime without breaking client code (e.g., removing a demonetized currency note handler).
9. Real-World Industry Use Cases
- Logging Frameworks (e.g., Log4j / Slf4j):
- Log levels (
DEBUG,INFO,WARN,ERROR,FATAL) form a chain. If a log level is configured asWARN, messages pass throughDEBUGandINFOhandlers (and are filtered out), finally reaching theWARNhandler which processes and outputs them.
- Log levels (
- Leave Approval Workflow Systems:
- Employees submit leave requests. The request first hits the Team Lead handler. If the duration is days, it's approved. Otherwise, it delegates to the Manager handler, then to the Director handler, and finally fails or passes upward if unfulfilled.
Summary and Key Takeaways
- Decoupled Senders and Receivers: The client initiating a request remains entirely unaware of which specific object processes it.
- Dynamic Chains: Handlers are chained together at runtime using shared abstract interfaces or base classes.
- Open-Closed Principle (OCP): New operational rules or handlers can be introduced seamlessly without modifying existing classes.
- Single Responsibility Principle (SRP): Each concrete handler encapsulates the logic for a single specific task or denomination.
- Recursive Delegation: Unfulfilled requests automatically cascade down the chain to subsequent handlers.
- Ideal for Hierarchical Workflows: Perfect for scenarios like logging pipelines, approval systems, and ATM dispensing logic.