Chain of Responsibility Pattern - Passing Requests Along Chain of Handlers
Learning Objectives: By the end of this lesson, you will be able to:
- Understand the Chain of Responsibility pattern and when to decouple request senders from receivers
- Implement handler chains with flexible request processing and dynamic configuration
- Apply Chain of Responsibility to middleware systems, event handling, and approval processes
- Design efficient chains with proper handler ordering and short-circuiting
- Handle complex scenarios including request modification, multiple handlers, and error propagation
1. Welcome and Introduction (5 minutes)
Icebreaker Activity
Think about a customer service call center. When you call with a problem, you might first talk to a front-line agent. If they can’t help, they escalate to a supervisor. If the supervisor can’t help, it goes to a specialist, and so on. Each person in the chain has a chance to handle your request, but if they can’t, they pass it along. How would you design a system that works similarly for processing different types of requests?
Lesson Overview
- Duration: 120 minutes
- Prerequisites: Object-oriented programming, basic understanding of linked data structures
- Materials Needed: Development environment with Java, Python, or JavaScript support
2. Learning Objectives and Session Outline (5 minutes)
What We’ll Cover Today:
- Chain Structure - Understanding handlers, successors, and request propagation
- Handler Design - Abstract handlers, concrete handlers, and responsibility determination
- Dynamic Configuration - Building and modifying chains at runtime
- Real-world Applications - Middleware, event systems, approval workflows, logging
- Performance Optimization - Handler ordering, caching, and short-circuiting
- Advanced Patterns - Multiple chains, request modification, error handling
Success Criteria:
By the end of this session, you should be able to design and implement flexible handler chains for various request processing scenarios.
3. Foundation Knowledge (15-20 minutes)
Core Concepts
What is the Chain of Responsibility Pattern?
The Chain of Responsibility pattern passes requests along a chain of handlers until one handles it. Each handler decides either to process the request or pass it to the next handler in the chain.
Real-World Analogy: Think of a corporate expense approval system. Small expenses might be approved by a team lead, medium expenses by a department manager, and large expenses by a director. Each person in the chain checks if they can approve the expense based on their authority level, and if not, they pass it up the chain.
Why This Matters: This pattern decouples the sender of a request from its receivers, giving multiple objects a chance to handle the request. It also allows you to vary the chain dynamically and add new handlers without changing existing code.
Key Components
- Handler: Abstract interface defining request handling method and successor management
- Concrete Handler: Implements handling logic for specific request types
- Client: Initiates requests to the chain
- Chain: The linked structure of handlers
Key Terminology
- Request: Data or command that needs to be processed
- Successor: Next handler in the chain
- Chain Composition: Process of linking handlers together
- Short-circuiting: Stopping chain execution when request is handled
- Pass-through: Request continues through chain even after being handled
- Request Modification: Handlers changing request data before passing it along
4. Detailed Explanation and Examples (25-30 minutes)
Understanding Chain of Responsibility Pattern Structure
The Chain of Responsibility pattern is essential when:
- Multiple objects can handle a request, but you don’t know which one until runtime
- You want to issue requests without specifying the receiver explicitly
- The set of objects that can handle requests should be specified dynamically
- You want to decouple senders from receivers
- You need flexibility in distributing responsibilities among objects
Practical Applications
Chain of Responsibility pattern is commonly used for:
- Web Middleware: Authentication, logging, compression, error handling in web frameworks
- Event Processing: GUI event bubbling, game event systems, notification chains
- Approval Workflows: Expense approval, document review, access request processing
- Logging Systems: Different log levels with multiple output destinations
- Input Validation: Form validation with multiple validation rules
- Security Systems: Multi-layer security checks, firewall rule processing
Common Misconceptions
-
Misconception: Chain of Responsibility always stops at first handler Reality: Can be configured for pass-through behavior where multiple handlers process the same request
-
Misconception: Handlers must be in a linear chain Reality: Can have branching or tree-like handler structures
5. Live Coding Demonstration (20-25 minutes)
Example 1: Expense Approval System
// Request class
class ExpenseRequest {
private String employeeName;
private String description;
private double amount;
private String category;
private Date submissionDate;
private boolean approved;
private String approvedBy;
private String rejectionReason;
public ExpenseRequest(String employeeName, String description, double amount, String category) {
this.employeeName = employeeName;
this.description = description;
this.amount = amount;
this.category = category;
this.submissionDate = new Date();
this.approved = false;
}
// Getters and setters
public String getEmployeeName() { return employeeName; }
public String getDescription() { return description; }
public double getAmount() { return amount; }
public String getCategory() { return category; }
public Date getSubmissionDate() { return submissionDate; }
public boolean isApproved() { return approved; }
public void setApproved(boolean approved) { this.approved = approved; }
public String getApprovedBy() { return approvedBy; }
public void setApprovedBy(String approvedBy) { this.approvedBy = approvedBy; }
public String getRejectionReason() { return rejectionReason; }
public void setRejectionReason(String rejectionReason) { this.rejectionReason = rejectionReason; }
@Override
public String toString() {
return String.format("ExpenseRequest{employee='%s', amount=%.2f, category='%s', description='%s'}",
employeeName, amount, category, description);
}
}
// Abstract Handler
abstract class ExpenseHandler {
protected ExpenseHandler successor;
protected String handlerName;
protected double approvalLimit;
public ExpenseHandler(String handlerName, double approvalLimit) {
this.handlerName = handlerName;
this.approvalLimit = approvalLimit;
}
public void setSuccessor(ExpenseHandler successor) {
this.successor = successor;
}
public final void handleRequest(ExpenseRequest request) {
if (canHandle(request)) {
processRequest(request);
} else if (successor != null) {
System.out.println(handlerName + " cannot handle $" + request.getAmount() +
" expense, passing to next handler");
successor.handleRequest(request);
} else {
System.out.println("No handler available for expense of $" + request.getAmount());
request.setRejectionReason("Exceeds maximum approval authority");
}
}
protected abstract boolean canHandle(ExpenseRequest request);
protected abstract void processRequest(ExpenseRequest request);
protected boolean isValidCategory(String category) {
return category != null && !category.trim().isEmpty();
}
protected boolean hasRequiredDocumentation(ExpenseRequest request) {
// Simplified check - in real system would verify receipts, etc.
return request.getDescription() != null && request.getDescription().length() >= 10;
}
}
// Concrete Handlers
class TeamLeadHandler extends ExpenseHandler {
public TeamLeadHandler() {
super("Team Lead", 500.0);
}
@Override
protected boolean canHandle(ExpenseRequest request) {
return request.getAmount() <= approvalLimit &&
isValidCategory(request.getCategory()) &&
hasRequiredDocumentation(request);
}
@Override
protected void processRequest(ExpenseRequest request) {
System.out.println("✅ " + handlerName + " approved expense: " + request);
request.setApproved(true);
request.setApprovedBy(handlerName);
// Additional processing for team lead level
if (request.getAmount() > 300) {
System.out.println(" Note: Expense over $300 will be flagged for audit");
}
}
}
class DepartmentManagerHandler extends ExpenseHandler {
public DepartmentManagerHandler() {
super("Department Manager", 2000.0);
}
@Override
protected boolean canHandle(ExpenseRequest request) {
return request.getAmount() <= approvalLimit &&
isValidCategory(request.getCategory()) &&
hasRequiredDocumentation(request) &&
!isSuspiciousCategory(request.getCategory());
}
@Override
protected void processRequest(ExpenseRequest request) {
System.out.println("✅ " + handlerName + " approved expense: " + request);
request.setApproved(true);
request.setApprovedBy(handlerName);
// Additional validation for manager level
if (request.getAmount() > 1000) {
System.out.println(" Note: Large expense approved, notification sent to director");
}
}
private boolean isSuspiciousCategory(String category) {
// Simplified suspicious category check
return category.toLowerCase().contains("entertainment") &&
Math.random() > 0.7; // 30% chance of requiring higher approval
}
}
class DirectorHandler extends ExpenseHandler {
public DirectorHandler() {
super("Director", 10000.0);
}
@Override
protected boolean canHandle(ExpenseRequest request) {
return request.getAmount() <= approvalLimit &&
isValidCategory(request.getCategory()) &&
hasRequiredDocumentation(request);
}
@Override
protected void processRequest(ExpenseRequest request) {
System.out.println("✅ " + handlerName + " approved expense: " + request);
request.setApproved(true);
request.setApprovedBy(handlerName);
// Director-level additional processing
if (request.getAmount() > 5000) {
System.out.println(" Note: Major expense approved, accounting department notified");
System.out.println(" Additional documentation may be required for tax purposes");
}
}
}
class CFOHandler extends ExpenseHandler {
public CFOHandler() {
super("CFO", Double.MAX_VALUE);
}
@Override
protected boolean canHandle(ExpenseRequest request) {
// CFO can handle any expense with proper documentation
return hasRequiredDocumentation(request);
}
@Override
protected void processRequest(ExpenseRequest request) {
if (request.getAmount() > 25000) {
System.out.println("⚠️ " + handlerName + " reviewing major expense: " + request);
// Simulate additional review process
System.out.println(" Initiating board review process for expenses over $25,000");
}
System.out.println("✅ " + handlerName + " approved expense: " + request);
request.setApproved(true);
request.setApprovedBy(handlerName);
}
}
Example 2: Web Request Middleware Chain
// Request and Response classes
class HttpRequest {
private String method;
private String path;
private Map<String, String> headers;
private Map<String, String> parameters;
private String body;
private Object user;
public HttpRequest(String method, String path) {
this.method = method;
this.path = path;
this.headers = new HashMap<>();
this.parameters = new HashMap<>();
}
// Getters and setters
public String getMethod() { return method; }
public String getPath() { return path; }
public Map<String, String> getHeaders() { return headers; }
public Map<String, String> getParameters() { return parameters; }
public String getBody() { return body; }
public void setBody(String body) { this.body = body; }
public Object getUser() { return user; }
public void setUser(Object user) { this.user = user; }
public void addHeader(String key, String value) { headers.put(key, value); }
public void addParameter(String key, String value) { parameters.put(key, value); }
public String getHeader(String key) { return headers.get(key); }
public String getParameter(String key) { return parameters.get(key); }
}
class HttpResponse {
private int statusCode;
private Map<String, String> headers;
private String body;
private boolean completed;
public HttpResponse() {
this.statusCode = 200;
this.headers = new HashMap<>();
this.completed = false;
}
// Getters and setters
public int getStatusCode() { return statusCode; }
public void setStatusCode(int statusCode) { this.statusCode = statusCode; }
public Map<String, String> getHeaders() { return headers; }
public String getBody() { return body; }
public void setBody(String body) { this.body = body; }
public boolean isCompleted() { return completed; }
public void setCompleted(boolean completed) { this.completed = completed; }
public void addHeader(String key, String value) { headers.put(key, value); }
public void sendError(int statusCode, String message) {
this.statusCode = statusCode;
this.body = message;
this.completed = true;
}
}
// Abstract Middleware Handler
abstract class MiddlewareHandler {
protected MiddlewareHandler next;
protected String name;
public MiddlewareHandler(String name) {
this.name = name;
}
public void setNext(MiddlewareHandler next) {
this.next = next;
}
public final void handle(HttpRequest request, HttpResponse response) {
if (response.isCompleted()) {
return; // Response already completed by previous middleware
}
System.out.println("Processing request through: " + name);
if (process(request, response)) {
if (!response.isCompleted() && next != null) {
next.handle(request, response);
}
}
// Post-processing (happens in reverse order)
postProcess(request, response);
}
protected abstract boolean process(HttpRequest request, HttpResponse response);
protected void postProcess(HttpRequest request, HttpResponse response) {
// Default implementation does nothing
// Subclasses can override for cleanup or response modification
}
}
// Concrete Middleware Handlers
class LoggingMiddleware extends MiddlewareHandler {
public LoggingMiddleware() {
super("Logging Middleware");
}
@Override
protected boolean process(HttpRequest request, HttpResponse response) {
String logEntry = String.format("[%s] %s %s - Headers: %d",
new Date(), request.getMethod(), request.getPath(),
request.getHeaders().size());
System.out.println("📝 LOG: " + logEntry);
return true; // Continue to next middleware
}
@Override
protected void postProcess(HttpRequest request, HttpResponse response) {
System.out.println("📝 LOG: Response " + response.getStatusCode() + " for " +
request.getMethod() + " " + request.getPath());
}
}
class AuthenticationMiddleware extends MiddlewareHandler {
private Set<String> validTokens;
public AuthenticationMiddleware() {
super("Authentication Middleware");
this.validTokens = new HashSet<>(Arrays.asList("token123", "token456", "adminToken"));
}
@Override
protected boolean process(HttpRequest request, HttpResponse response) {
String authHeader = request.getHeader("Authorization");
if (authHeader == null) {
response.sendError(401, "Missing authorization header");
System.out.println("🔒 AUTH: Request rejected - no auth header");
return false;
}
String token = authHeader.replace("Bearer ", "");
if (!validTokens.contains(token)) {
response.sendError(401, "Invalid token");
System.out.println("🔒 AUTH: Request rejected - invalid token");
return false;
}
// Set user information
request.setUser("User_" + token);
System.out.println("🔒 AUTH: Request authenticated for user: " + request.getUser());
return true;
}
}
class RateLimitingMiddleware extends MiddlewareHandler {
private Map<String, List<Long>> requestTimes;
private int maxRequestsPerMinute;
public RateLimitingMiddleware(int maxRequestsPerMinute) {
super("Rate Limiting Middleware");
this.requestTimes = new HashMap<>();
this.maxRequestsPerMinute = maxRequestsPerMinute;
}
@Override
protected boolean process(HttpRequest request, HttpResponse response) {
String clientId = getClientId(request);
long currentTime = System.currentTimeMillis();
requestTimes.putIfAbsent(clientId, new ArrayList<>());
List<Long> times = requestTimes.get(clientId);
// Remove requests older than 1 minute
times.removeIf(time -> currentTime - time > 60000);
if (times.size() >= maxRequestsPerMinute) {
response.sendError(429, "Rate limit exceeded");
System.out.println("⏱️ RATE LIMIT: Request rejected for client: " + clientId);
return false;
}
times.add(currentTime);
System.out.println("⏱️ RATE LIMIT: Request allowed for client: " + clientId +
" (" + times.size() + "/" + maxRequestsPerMinute + ")");
return true;
}
private String getClientId(HttpRequest request) {
// In real implementation, would use IP address or user ID
return request.getUser() != null ? request.getUser().toString() : "anonymous";
}
}
class CompressionMiddleware extends MiddlewareHandler {
public CompressionMiddleware() {
super("Compression Middleware");
}
@Override
protected boolean process(HttpRequest request, HttpResponse response) {
String acceptEncoding = request.getHeader("Accept-Encoding");
if (acceptEncoding != null && acceptEncoding.contains("gzip")) {
response.addHeader("Content-Encoding", "gzip");
System.out.println("🗜️ COMPRESSION: Gzip compression enabled");
} else {
System.out.println("🗜️ COMPRESSION: No compression applied");
}
return true;
}
@Override
protected void postProcess(HttpRequest request, HttpResponse response) {
String contentEncoding = response.getHeaders().get("Content-Encoding");
if ("gzip".equals(contentEncoding) && response.getBody() != null) {
// Simulate compression
int originalSize = response.getBody().length();
String compressedBody = "[COMPRESSED]" + response.getBody();
response.setBody(compressedBody);
System.out.println("🗜️ COMPRESSION: Body compressed from " + originalSize +
" to " + compressedBody.length() + " bytes");
}
}
}
class ValidationMiddleware extends MiddlewareHandler {
public ValidationMiddleware() {
super("Validation Middleware");
}
@Override
protected boolean process(HttpRequest request, HttpResponse response) {
// Validate request method
if (!isValidMethod(request.getMethod())) {
response.sendError(405, "Method not allowed: " + request.getMethod());
System.out.println("❌ VALIDATION: Invalid method: " + request.getMethod());
return false;
}
// Validate path
if (!isValidPath(request.getPath())) {
response.sendError(400, "Invalid path: " + request.getPath());
System.out.println("❌ VALIDATION: Invalid path: " + request.getPath());
return false;
}
// Validate content type for POST/PUT requests
if (("POST".equals(request.getMethod()) || "PUT".equals(request.getMethod())) &&
request.getHeader("Content-Type") == null) {
response.sendError(400, "Content-Type header required for " + request.getMethod());
System.out.println("❌ VALIDATION: Missing Content-Type for " + request.getMethod());
return false;
}
System.out.println("✅ VALIDATION: Request passed validation");
return true;
}
private boolean isValidMethod(String method) {
return Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH").contains(method);
}
private boolean isValidPath(String path) {
return path != null && path.startsWith("/") && !path.contains("..");
}
}
Example 3: Event Processing Chain
// Event class
class Event {
private String type;
private Object source;
private Map<String, Object> data;
private boolean consumed;
private long timestamp;
public Event(String type, Object source) {
this.type = type;
this.source = source;
this.data = new HashMap<>();
this.consumed = false;
this.timestamp = System.currentTimeMillis();
}
// Getters and setters
public String getType() { return type; }
public Object getSource() { return source; }
public Map<String, Object> getData() { return data; }
public boolean isConsumed() { return consumed; }
public void setConsumed(boolean consumed) { this.consumed = consumed; }
public long getTimestamp() { return timestamp; }
public void setData(String key, Object value) { data.put(key, value); }
public Object getData(String key) { return data.get(key); }
}
// Abstract Event Handler
abstract class EventHandler {
protected EventHandler successor;
protected String handlerName;
protected Set<String> handledEventTypes;
public EventHandler(String handlerName, String... eventTypes) {
this.handlerName = handlerName;
this.handledEventTypes = new HashSet<>(Arrays.asList(eventTypes));
}
public void setSuccessor(EventHandler successor) {
this.successor = successor;
}
public final void handleEvent(Event event) {
if (canHandle(event)) {
System.out.println(handlerName + " handling event: " + event.getType());
processEvent(event);
}
// Continue chain unless event is consumed
if (!event.isConsumed() && successor != null) {
successor.handleEvent(event);
}
}
protected boolean canHandle(Event event) {
return handledEventTypes.contains(event.getType());
}
protected abstract void processEvent(Event event);
}
// Concrete Event Handlers
class SecurityEventHandler extends EventHandler {
public SecurityEventHandler() {
super("Security Handler", "login", "logout", "failed_login", "permission_denied");
}
@Override
protected void processEvent(Event event) {
switch (event.getType()) {
case "login":
logSecurityEvent("User logged in: " + event.getData("username"));
break;
case "logout":
logSecurityEvent("User logged out: " + event.getData("username"));
break;
case "failed_login":
logSecurityEvent("Failed login attempt: " + event.getData("username"));
checkForBruteForce(event);
break;
case "permission_denied":
logSecurityEvent("Permission denied for: " + event.getData("username"));
break;
}
}
private void logSecurityEvent(String message) {
System.out.println("🔐 SECURITY LOG: " + message);
}
private void checkForBruteForce(Event event) {
// Simplified brute force detection
System.out.println("🔐 SECURITY: Checking for brute force attack patterns");
}
}
class AuditEventHandler extends EventHandler {
private List<String> auditLog;
public AuditEventHandler() {
super("Audit Handler", "data_access", "data_modification", "admin_action");
this.auditLog = new ArrayList<>();
}
@Override
protected void processEvent(Event event) {
String auditEntry = createAuditEntry(event);
auditLog.add(auditEntry);
System.out.println("📋 AUDIT: " + auditEntry);
// For critical events, also send to external audit system
if (isCriticalEvent(event)) {
System.out.println("📋 AUDIT: Critical event flagged for external review");
}
}
private String createAuditEntry(Event event) {
return String.format("[%s] %s by %s - Details: %s",
new Date(event.getTimestamp()),
event.getType(),
event.getData("username"),
event.getData().toString());
}
private boolean isCriticalEvent(Event event) {
return "admin_action".equals(event.getType()) ||
("data_modification".equals(event.getType()) &&
"sensitive".equals(event.getData("data_classification")));
}
public List<String> getAuditLog() {
return new ArrayList<>(auditLog);
}
}
class NotificationEventHandler extends EventHandler {
public NotificationEventHandler() {
super("Notification Handler", "user_message", "system_alert", "error");
}
@Override
protected void processEvent(Event event) {
switch (event.getType()) {
case "user_message":
sendUserNotification(event);
break;
case "system_alert":
sendSystemAlert(event);
break;
case "error":
sendErrorNotification(event);
event.setConsumed(true); // Stop further processing for errors
break;
}
}
private void sendUserNotification(Event event) {
String recipient = (String) event.getData("recipient");
String message = (String) event.getData("message");
System.out.println("📧 NOTIFICATION: Sending to " + recipient + ": " + message);
}
private void sendSystemAlert(Event event) {
String alertLevel = (String) event.getData("level");
String message = (String) event.getData("message");
System.out.println("🚨 SYSTEM ALERT [" + alertLevel + "]: " + message);
}
private void sendErrorNotification(Event event) {
String error = (String) event.getData("error");
System.out.println("❌ ERROR NOTIFICATION: " + error);
System.out.println("📧 ERROR: Notifying system administrators");
}
}
Example 4: Usage Demonstrations
public class ChainOfResponsibilityDemo {
public static void demonstrateExpenseApproval() {
System.out.println("=== Expense Approval Chain ===");
// Build the chain
ExpenseHandler teamLead = new TeamLeadHandler();
ExpenseHandler manager = new DepartmentManagerHandler();
ExpenseHandler director = new DirectorHandler();
ExpenseHandler cfo = new CFOHandler();
teamLead.setSuccessor(manager);
manager.setSuccessor(director);
director.setSuccessor(cfo);
// Test different expense amounts
ExpenseRequest[] requests = {
new ExpenseRequest("John Doe", "Office supplies", 250.0, "supplies"),
new ExpenseRequest("Jane Smith", "Client dinner", 850.0, "entertainment"),
new ExpenseRequest("Bob Johnson", "Conference attendance", 2500.0, "training"),
new ExpenseRequest("Alice Brown", "New server equipment", 12000.0, "equipment"),
new ExpenseRequest("Charlie Wilson", "Company retreat", 50000.0, "events")
};
for (ExpenseRequest request : requests) {
System.out.println("\nProcessing: " + request);
teamLead.handleRequest(request);
System.out.println("Result: " + (request.isApproved() ?
"✅ Approved by " + request.getApprovedBy() :
"❌ Rejected - " + request.getRejectionReason()));
}
}
public static void demonstrateWebMiddleware() {
System.out.println("\n=== Web Middleware Chain ===");
// Build middleware chain
MiddlewareHandler logging = new LoggingMiddleware();
MiddlewareHandler rateLimit = new RateLimitingMiddleware(5);
MiddlewareHandler auth = new AuthenticationMiddleware();
MiddlewareHandler validation = new ValidationMiddleware();
MiddlewareHandler compression = new CompressionMiddleware();
logging.setNext(rateLimit);
rateLimit.setNext(validation);
validation.setNext(auth);
auth.setNext(compression);
// Test requests
HttpRequest[] requests = {
createRequest("GET", "/api/users", "token123"),
createRequest("POST", "/api/users", null), // No auth
createRequest("INVALID", "/api/users", "token456"), // Invalid method
createRequest("GET", "/api/data", "adminToken")
};
for (HttpRequest request : requests) {
System.out.println("\n--- Processing Request: " + request.getMethod() + " " + request.getPath() + " ---");
HttpResponse response = new HttpResponse();
logging.handle(request, response);
if (!response.isCompleted()) {
response.setBody("Request processed successfully");
}
System.out.println("Final Response: " + response.getStatusCode() +
(response.getBody() != null ? " - " + response.getBody() : ""));
}
}
private static HttpRequest createRequest(String method, String path, String token) {
HttpRequest request = new HttpRequest(method, path);
request.addHeader("Accept-Encoding", "gzip");
if (token != null) {
request.addHeader("Authorization", "Bearer " + token);
}
if ("POST".equals(method) || "PUT".equals(method)) {
request.addHeader("Content-Type", "application/json");
}
return request;
}
public static void demonstrateEventProcessing() {
System.out.println("\n=== Event Processing Chain ===");
// Build event handler chain
EventHandler security = new SecurityEventHandler();
EventHandler audit = new AuditEventHandler();
EventHandler notification = new NotificationEventHandler();
security.setSuccessor(audit);
audit.setSuccessor(notification);
// Test events
Event[] events = {
createEvent("login", "username", "john_doe"),
createEvent("data_access", "username", "jane_smith", "resource", "customer_data"),
createEvent("failed_login", "username", "attacker"),
createEvent("error", "error", "Database connection failed", "severity", "high"),
createEvent("admin_action", "username", "admin", "action", "user_deleted", "data_classification", "sensitive")
};
for (Event event : events) {
System.out.println("\n--- Processing Event: " + event.getType() + " ---");
security.handleEvent(event);
}
}
private static Event createEvent(String type, String... keyValuePairs) {
Event event = new Event(type, "system");
for (int i = 0; i < keyValuePairs.length; i += 2) {
event.setData(keyValuePairs[i], keyValuePairs[i + 1]);
}
return event;
}
public static void main(String[] args) {
demonstrateExpenseApproval();
demonstrateWebMiddleware();
demonstrateEventProcessing();
}
}
6. Hands-On Practice (30-40 minutes)
Exercise 1: Basic Practice (10 minutes)
Objective: Implement a simple help desk ticket escalation chain
Instructions:
- Create a SupportTicket class with priority levels (Low, Medium, High, Critical)
- Implement handlers: Level1Support, Level2Support, Level3Support
- Each level can handle tickets up to a certain priority
- Add timing constraints (Level1 has 2 hours, Level2 has 8 hours, etc.)
Starter Code:
class SupportTicket {
private String ticketId;
private String description;
private Priority priority;
private Date submissionTime;
enum Priority { LOW, MEDIUM, HIGH, CRITICAL }
// TODO: Implement constructor and methods
}
abstract class SupportHandler {
// TODO: Implement chain structure and handling logic
}
Exercise 2: Intermediate Challenge (15 minutes)
Objective: Build a content moderation chain for a social media platform
Requirements:
- Handle different content types: text, image, video, link
- Implement filters: profanity, spam, inappropriate content, copyright
- Each filter can approve, flag for review, or reject content
- Add confidence scores and human review thresholds
- Support filter bypasses for verified users
Hints:
- Consider using enums for content types and moderation actions
- Think about how confidence scores accumulate through the chain
- Design for easy addition of new filters
Exercise 3: Creative Application (15 minutes)
Objective: Design a smart home automation chain
Challenge: Create a chain that processes home automation events:
- Security events (motion detected, door opened, alarm triggered)
- Environmental events (temperature change, humidity, air quality)
- Energy events (high usage, power outage, solar generation)
- Each handler can respond, log, notify, or trigger other automations
Extension Ideas:
- Add priority-based processing for emergency events
- Implement device-specific handlers
- Create conditional chains based on time of day or occupancy
7. Common Pitfalls and Troubleshooting (10 minutes)
Frequent Mistakes
-
Problem: Chain becomes too long and inefficient Cause: Not organizing handlers by probability of handling requests Solution: Put most likely handlers first, profile and optimize chain order
-
Problem: Circular dependencies in handler chains Cause: Setting up chains where handlers reference each other incorrectly Solution: Use builder pattern or factory to construct chains safely
-
Problem: Request modification causes unexpected behavior Cause: Handlers modifying shared request objects Solution: Use immutable requests or clearly document modification contracts
Debugging Tips
- Add detailed logging to track request flow through the chain
- Implement chain visualization tools for complex chains
- Use metrics to identify bottlenecks and frequently used handlers
- Test with edge cases like null requests and empty chains
8. Best Practices and Professional Tips (5 minutes)
Industry Standards
- Order handlers by likelihood - most probable handlers first for performance
- Keep handlers stateless - avoid side effects between requests
- Document handler responsibilities clearly to prevent overlap
- Use metrics and monitoring to optimize chain performance
Design Principles
- Single Responsibility - each handler should have one clear purpose
- Open/Closed Principle - easy to add new handlers without modifying existing ones
- Fail fast - reject invalid requests early in the chain
- Graceful degradation - handle missing handlers elegantly
Performance Considerations
- Cache handler lookup for frequently used chains
- Implement short-circuiting when appropriate to stop processing early
- Profile chain execution to identify slow handlers
- Consider parallel processing for independent handlers
Next Steps in Your Learning Journey
- Study Command pattern for creating undoable chain operations
- Learn Observer pattern for chain event notifications
- Explore Composite pattern for hierarchical handler structures
- Investigate middleware frameworks like Express.js or Spring Security
9. Summary and Review (5 minutes)
Key Takeaways
- Chain of Responsibility decouples senders from receivers allowing flexible request handling
- Dynamic chain composition enables runtime configuration of request processing
- Perfect for middleware systems where multiple processors might handle the same request
- Order matters - organize chains for optimal performance and correctness
Quick Review Questions
- When would you use Chain of Responsibility vs Strategy pattern?
- How do you handle the case where no handler in the chain can process a request?
- What are the trade-offs between linear chains vs tree-like handler structures?
For Instructors
Teaching Tips
- Start with familiar examples (customer service, corporate approval) before technical patterns
- Show both stopping and pass-through behaviors to demonstrate flexibility
- Use visual diagrams to show request flow through chains
- Emphasize decoupling benefits - how easy it is to add/remove handlers
- Live code the chain construction to show dynamic configuration
Common Student Questions
- “When to stop vs continue the chain?” - Discuss different use cases and design decisions
- “How is this different from decorators?” - Explain request handling vs behavior enhancement
- “What about performance with long chains?” - Discuss optimization strategies
Assessment Ideas
- Design challenge: Create chain for specific domain (payment processing, content filtering)
- Performance analysis: Compare chain processing vs direct conditional logic
- Refactoring exercise: Convert switch statement logic to chain of responsibility
Adaptation Notes
- Beginner focus: Start with simple linear chains before complex branching
- Advanced students: Discuss concurrent chains, priority-based processing, and performance optimization
- Language-specific: Show language-specific implementations (function chains, lambda expressions)
- Time management: Event processing example can be simplified for shorter sessions