Introduction
The Chain of Responsibility Pattern is a behavioral design pattern that allows passing 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.
Core Concept
Definition
Chain of Responsibility Pattern lets you pass requests along a chain of handlers. Each handler decides either to process the request or to pass it to the next handler in the chain.
When to Use
- When you want to give more than one object a chance to handle a request
- When you want to decouple the sender and receiver of a request
- When multiple objects can handle a request, but the handler isn't known beforehand
- When you want to dynamically specify handlers at runtime
Basic Implementation
1. Handler Interface
// Handler interface public interface Handler { void setNext(Handler handler); void handle(Request request); } // Request class public class Request { private String type; private String content; private int severity; // 1-10, where 10 is most severe public Request(String type, String content, int severity) { this.type = type; this.content = content; this.severity = severity; } // Getters and setters public String getType() { return type; } public String getContent() { return content; } public int getSeverity() { return severity; } public void setType(String type) { this.type = type; } public void setContent(String content) { this.content = content; } public void setSeverity(int severity) { this.severity = severity; } @Override public String toString() { return String.format("Request{type='%s', content='%s', severity=%d}", type, content, severity); } } 2. Abstract Base Handler
// Abstract base handler public abstract class AbstractHandler implements Handler { private Handler nextHandler; @Override public void setNext(Handler handler) { this.nextHandler = handler; } @Override public void handle(Request request) { if (nextHandler != null) { nextHandler.handle(request); } else { // End of chain - no handler could process the request System.out.println("No handler available for: " + request); } } // Template method pattern - subclasses implement canHandle and process protected abstract boolean canHandle(Request request); protected abstract void process(Request request); } 3. Concrete Handlers
// Concrete handler for low severity requests public class LowSeverityHandler extends AbstractHandler { @Override protected boolean canHandle(Request request) { return request.getSeverity() <= 3; } @Override protected void process(Request request) { System.out.println("LowSeverityHandler processing: " + request.getContent()); System.out.println("-> Assigned to Junior Support"); } @Override public void handle(Request request) { if (canHandle(request)) { process(request); } else { super.handle(request); // Pass to next handler } } } // Concrete handler for medium severity requests public class MediumSeverityHandler extends AbstractHandler { @Override protected boolean canHandle(Request request) { return request.getSeverity() <= 7; } @Override protected void process(Request request) { System.out.println("MediumSeverityHandler processing: " + request.getContent()); System.out.println("-> Assigned to Senior Support"); } @Override public void handle(Request request) { if (canHandle(request)) { process(request); } else { super.handle(request); } } } // Concrete handler for high severity requests public class HighSeverityHandler extends AbstractHandler { @Override protected boolean canHandle(Request request) { return request.getSeverity() <= 10; } @Override protected void process(Request request) { System.out.println("HighSeverityHandler processing: " + request.getContent()); System.out.println("-> Escalated to Management"); } @Override public void handle(Request request) { if (canHandle(request)) { process(request); } else { super.handle(request); } } } 4. Client Code
// Client code public class ChainOfResponsibilityDemo { public static void main(String[] args) { // Create handlers Handler lowHandler = new LowSeverityHandler(); Handler mediumHandler = new MediumSeverityHandler(); Handler highHandler = new HighSeverityHandler(); // Build the chain lowHandler.setNext(mediumHandler); mediumHandler.setNext(highHandler); // Create requests with different severity levels Request[] requests = { new Request("BUG", "Button color is wrong", 2), new Request("FEATURE", "Add new payment method", 5), new Request("CRITICAL", "System crash on startup", 9), new Request("SECURITY", "Data breach detected", 10) }; // Process requests System.out.println("Processing requests through chain:\n"); for (Request request : requests) { System.out.println("=== Processing: " + request + " ==="); lowHandler.handle(request); System.out.println(); } } } Advanced Implementation Variations
1. Handler with Multiple Conditions
// Advanced handler with multiple conditions public class TechnicalSupportHandler extends AbstractHandler { private List<String> supportedTypes = Arrays.asList("BUG", "TECHNICAL", "PERFORMANCE"); @Override protected boolean canHandle(Request request) { return supportedTypes.contains(request.getType()) && request.getSeverity() <= 6; } @Override protected void process(Request request) { System.out.println("TechnicalSupportHandler processing: " + request.getContent()); // Different processing based on type switch (request.getType()) { case "BUG": System.out.println("-> Creating bug ticket in JIRA"); break; case "TECHNICAL": System.out.println("-> Assigning to technical team"); break; case "PERFORMANCE": System.out.println("-> Running performance analysis"); break; } } @Override public void handle(Request request) { if (canHandle(request)) { process(request); } else { super.handle(request); } } } public class BillingSupportHandler extends AbstractHandler { @Override protected boolean canHandle(Request request) { return "BILLING".equals(request.getType()) || "PAYMENT".equals(request.getType()); } @Override protected void process(Request request) { System.out.println("BillingSupportHandler processing: " + request.getContent()); System.out.println("-> Forwarding to finance department"); } @Override public void handle(Request request) { if (canHandle(request)) { process(request); } else { super.handle(request); } } } 2. Chain Builder for Flexible Configuration
// Chain builder for easy chain configuration public class HandlerChainBuilder { private Handler firstHandler; private Handler lastHandler; public HandlerChainBuilder addHandler(Handler handler) { if (firstHandler == null) { firstHandler = handler; lastHandler = handler; } else { lastHandler.setNext(handler); lastHandler = handler; } return this; } public Handler build() { return firstHandler; } } // Usage public class AdvancedChainDemo { public static void main(String[] args) { // Build chain using builder Handler chain = new HandlerChainBuilder() .addHandler(new TechnicalSupportHandler()) .addHandler(new BillingSupportHandler()) .addHandler(new LowSeverityHandler()) .addHandler(new MediumSeverityHandler()) .addHandler(new HighSeverityHandler()) .build(); // Test various requests Request[] requests = { new Request("BUG", "Login page not loading", 3), new Request("BILLING", "Invoice discrepancy", 4), new Request("FEATURE", "New dashboard design", 2), new Request("TECHNICAL", "Database connection issues", 7) }; System.out.println("Processing mixed requests:\n"); for (Request request : requests) { System.out.println("=== Processing: " + request + " ==="); chain.handle(request); System.out.println(); } } } Real-World Examples
1. Logging Framework
// Logging level enum public enum LogLevel { DEBUG(1), INFO(2), WARN(3), ERROR(4), FATAL(5); private final int level; LogLevel(int level) { this.level = level; } public int getLevel() { return level; } } // Log message public class LogMessage { private LogLevel level; private String message; private String loggerName; private long timestamp; public LogMessage(LogLevel level, String message, String loggerName) { this.level = level; this.message = message; this.loggerName = loggerName; this.timestamp = System.currentTimeMillis(); } // Getters public LogLevel getLevel() { return level; } public String getMessage() { return message; } public String getLoggerName() { return loggerName; } public long getTimestamp() { return timestamp; } } // Logger handler public abstract class LoggerHandler { private LoggerHandler next; protected LogLevel level; public LoggerHandler(LogLevel level) { this.level = level; } public void setNext(LoggerHandler next) { this.next = next; } public void log(LogMessage message) { if (message.getLevel().getLevel() >= this.level.getLevel()) { writeMessage(message); } if (next != null) { next.log(message); } } protected abstract void writeMessage(LogMessage message); } // Concrete loggers public class ConsoleLogger extends LoggerHandler { public ConsoleLogger(LogLevel level) { super(level); } @Override protected void writeMessage(LogMessage message) { System.out.printf("[CONSOLE] %s - %s: %s%n", message.getLevel(), message.getLoggerName(), message.getMessage()); } } public class FileLogger extends LoggerHandler { private String filename; public FileLogger(LogLevel level, String filename) { super(level); this.filename = filename; } @Override protected void writeMessage(LogMessage message) { // Simulate file writing System.out.printf("[FILE:%s] %s - %s: %s%n", filename, message.getLevel(), message.getLoggerName(), message.getMessage()); } } public class DatabaseLogger extends LoggerHandler { public DatabaseLogger(LogLevel level) { super(level); } @Override protected void writeMessage(LogMessage message) { // Simulate database insertion System.out.printf("[DATABASE] %s - %s: %s%n", message.getLevel(), message.getLoggerName(), message.getMessage()); } } // Logging client public class LoggingSystem { private LoggerHandler loggerChain; public LoggingSystem() { setupLoggerChain(); } private void setupLoggerChain() { LoggerHandler consoleLogger = new ConsoleLogger(LogLevel.DEBUG); LoggerHandler fileLogger = new FileLogger(LogLevel.INFO, "app.log"); LoggerHandler dbLogger = new DatabaseLogger(LogLevel.ERROR); consoleLogger.setNext(fileLogger); fileLogger.setNext(dbLogger); loggerChain = consoleLogger; } public void log(LogLevel level, String loggerName, String message) { LogMessage logMessage = new LogMessage(level, message, loggerName); loggerChain.log(logMessage); } // Convenience methods public void debug(String loggerName, String message) { log(LogLevel.DEBUG, loggerName, message); } public void info(String loggerName, String message) { log(LogLevel.INFO, loggerName, message); } public void error(String loggerName, String message) { log(LogLevel.ERROR, loggerName, message); } } // Demo public class LoggingDemo { public static void main(String[] args) { LoggingSystem loggingSystem = new LoggingSystem(); System.out.println("Testing logging chain:\n"); loggingSystem.debug("UserService", "User login attempted"); loggingSystem.info("OrderService", "Order created successfully"); loggingSystem.error("PaymentService", "Payment gateway timeout"); loggingSystem.log(LogLevel.FATAL, "AuthService", "Security breach detected"); } } 2. Authentication and Authorization Chain
// Authentication request public class AuthRequest { private String username; private String password; private String token; private String resource; private String action; public AuthRequest(String username, String password, String token, String resource, String action) { this.username = username; this.password = password; this.token = token; this.resource = resource; this.action = action; } // Getters public String getUsername() { return username; } public String getPassword() { return password; } public String getToken() { return token; } public String getResource() { return resource; } public String getAction() { return action; } } // Authentication result public class AuthResult { private boolean authenticated; private boolean authorized; private String message; private String userRole; public AuthResult(boolean authenticated, boolean authorized, String message, String userRole) { this.authenticated = authenticated; this.authorized = authorized; this.message = message; this.userRole = userRole; } // Getters and setters public boolean isAuthenticated() { return authenticated; } public boolean isAuthorized() { return authorized; } public String getMessage() { return message; } public String getUserRole() { return userRole; } public void setAuthenticated(boolean authenticated) { this.authenticated = authenticated; } public void setAuthorized(boolean authorized) { this.authorized = authorized; } public void setMessage(String message) { this.message = message; } public void setUserRole(String userRole) { this.userRole = userRole; } } // Authentication handler public abstract class AuthHandler { private AuthHandler next; public void setNext(AuthHandler next) { this.next = next; } public AuthResult handle(AuthRequest request, AuthResult result) { // Process current handler process(request, result); // Pass to next handler if exists and processing should continue if (next != null && shouldContinue(request, result)) { return next.handle(request, result); } return result; } protected abstract void process(AuthRequest request, AuthResult result); protected abstract boolean shouldContinue(AuthRequest request, AuthResult result); } // Concrete authentication handlers public class TokenValidationHandler extends AuthHandler { @Override protected void process(AuthRequest request, AuthResult result) { if (request.getToken() != null && !request.getToken().isEmpty()) { // Simulate token validation if (isValidToken(request.getToken())) { result.setAuthenticated(true); result.setUserRole("API_USER"); result.setMessage("Token validation successful"); } else { result.setMessage("Invalid token"); } } } @Override protected boolean shouldContinue(AuthRequest request, AuthResult result) { // Continue only if token validation failed return !result.isAuthenticated(); } private boolean isValidToken(String token) { // Simulate token validation logic return token.startsWith("valid_"); } } public class CredentialValidationHandler extends AuthHandler { @Override protected void process(AuthRequest request, AuthResult result) { if (!result.isAuthenticated() && request.getUsername() != null && request.getPassword() != null) { // Simulate credential validation if (isValidCredentials(request.getUsername(), request.getPassword())) { result.setAuthenticated(true); result.setUserRole(getUserRole(request.getUsername())); result.setMessage("Credential validation successful"); } else { result.setMessage("Invalid credentials"); } } } @Override protected boolean shouldContinue(AuthRequest request, AuthResult result) { // Continue to authorization if authenticated return result.isAuthenticated(); } private boolean isValidCredentials(String username, String password) { // Simulate credential check return "admin".equals(username) && "password".equals(password) || "user".equals(username) && "123456".equals(password); } private String getUserRole(String username) { return "admin".equals(username) ? "ADMIN" : "USER"; } } public class AuthorizationHandler extends AuthHandler { private Map<String, List<String>> permissions = new HashMap<>(); public AuthorizationHandler() { // Initialize permissions permissions.put("ADMIN", Arrays.asList("READ", "WRITE", "DELETE", "EXECUTE")); permissions.put("USER", Arrays.asList("READ", "WRITE")); permissions.put("API_USER", Arrays.asList("READ")); } @Override protected void process(AuthRequest request, AuthResult result) { if (result.isAuthenticated()) { String userRole = result.getUserRole(); String action = request.getAction(); if (hasPermission(userRole, action)) { result.setAuthorized(true); result.setMessage("Authorization granted for " + action); } else { result.setAuthorized(false); result.setMessage("Insufficient permissions for " + action); } } } @Override protected boolean shouldContinue(AuthRequest request, AuthResult result) { // Stop chain after authorization return false; } private boolean hasPermission(String role, String action) { return permissions.containsKey(role) && permissions.get(role).contains(action.toUpperCase()); } } // Authentication system public class AuthenticationSystem { private AuthHandler authChain; public AuthenticationSystem() { setupAuthChain(); } private void setupAuthChain() { AuthHandler tokenHandler = new TokenValidationHandler(); AuthHandler credentialHandler = new CredentialValidationHandler(); AuthHandler authorizationHandler = new AuthorizationHandler(); tokenHandler.setNext(credentialHandler); credentialHandler.setNext(authorizationHandler); authChain = tokenHandler; } public AuthResult authenticate(AuthRequest request) { AuthResult initialResult = new AuthResult(false, false, "Not authenticated", null); return authChain.handle(request, initialResult); } } // Demo public class AuthDemo { public static void main(String[] args) { AuthenticationSystem authSystem = new AuthenticationSystem(); System.out.println("Testing authentication chain:\n"); // Test cases AuthRequest[] requests = { // Valid token, read access new AuthRequest(null, null, "valid_token123", "users", "READ"), // Admin credentials, full access new AuthRequest("admin", "password", null, "system", "DELETE"), // User credentials, limited access new AuthRequest("user", "123456", null, "reports", "READ"), // Invalid credentials new AuthRequest("user", "wrongpass", null, "data", "READ"), // No authentication new AuthRequest(null, null, null, "public", "READ") }; for (AuthRequest request : requests) { System.out.println("=== Processing Auth Request ==="); System.out.println("Username: " + request.getUsername()); System.out.println("Resource: " + request.getResource()); System.out.println("Action: " + request.getAction()); AuthResult result = authSystem.authenticate(request); System.out.println("Result:"); System.out.println(" Authenticated: " + result.isAuthenticated()); System.out.println(" Authorized: " + result.isAuthorized()); System.out.println(" Role: " + result.getUserRole()); System.out.println(" Message: " + result.getMessage()); System.out.println(); } } } Advanced Patterns with Chain of Responsibility
1. Chain with Break Conditions
// Handler that can break the chain public abstract class BreakableHandler extends AbstractHandler { protected boolean shouldBreakChain(Request request) { return false; // Default - don't break chain } @Override public void handle(Request request) { if (canHandle(request)) { process(request); // Check if chain should break after processing if (shouldBreakChain(request)) { return; // Break the chain } } // Continue to next handler super.handle(request); } } // Emergency handler that breaks the chain public class EmergencyHandler extends BreakableHandler { @Override protected boolean canHandle(Request request) { return "EMERGENCY".equals(request.getType()) || request.getSeverity() == 10; } @Override protected void process(Request request) { System.out.println("🚨 EMERGENCY HANDLER ACTIVATED!"); System.out.println("Processing: " + request.getContent()); System.out.println("-> Immediate action required!"); } @Override protected boolean shouldBreakChain(Request request) { // Always break chain for emergencies return true; } } 2. Chain with Request Modification
// Handler that can modify the request public abstract class ModifyingHandler extends AbstractHandler { @Override public void handle(Request request) { // Pre-processing: modify request if needed Request modifiedRequest = preProcess(request); if (canHandle(modifiedRequest)) { process(modifiedRequest); } else { // Pass modified request to next handler if (nextHandler != null) { nextHandler.handle(modifiedRequest); } } } protected Request preProcess(Request request) { // Default implementation - no modification return request; } } // Handler that enriches request with additional data public class RequestEnricher extends ModifyingHandler { @Override protected Request preProcess(Request request) { // Add timestamp and request ID String enrichedContent = String.format("[%s] %s", java.time.LocalDateTime.now(), request.getContent()); Request enriched = new Request( request.getType(), enrichedContent, request.getSeverity() ); return enriched; } @Override protected boolean canHandle(Request request) { // This handler doesn't actually process, just enriches return false; } @Override protected void process(Request request) { // Not used since canHandle always returns false } } 3. Chain with Parallel Processing
// Handler that processes in parallel public class ParallelHandler implements Handler { private List<Handler> parallelHandlers = new ArrayList<>(); public void addHandler(Handler handler) { parallelHandlers.add(handler); } @Override public void setNext(Handler handler) { // Not used in parallel handler } @Override public void handle(Request request) { // Process with all parallel handlers List<Thread> threads = new ArrayList<>(); for (Handler handler : parallelHandlers) { Thread thread = new Thread(() -> { try { handler.handle(request); } catch (Exception e) { System.err.println("Handler failed: " + e.getMessage()); } }); threads.add(thread); thread.start(); } // Wait for all threads to complete for (Thread thread : threads) { try { thread.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } } // Usage example public class ParallelChainDemo { public static void main(String[] args) { ParallelHandler parallelHandler = new ParallelHandler(); parallelHandler.addHandler(new LowSeverityHandler()); parallelHandler.addHandler(new TechnicalSupportHandler()); parallelHandler.addHandler(new BillingSupportHandler()); Request request = new Request("MIXED", "Multiple aspects issue", 5); System.out.println("Processing with parallel handlers:"); parallelHandler.handle(request); } } Testing Chain of Responsibility
Unit Testing Handlers
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeEach; import static org.junit.jupiter.api.Assertions.*; public class HandlerTest { private Handler lowHandler; private Handler mediumHandler; private Handler highHandler; @BeforeEach void setUp() { lowHandler = new LowSeverityHandler(); mediumHandler = new MediumSeverityHandler(); highHandler = new HighSeverityHandler(); lowHandler.setNext(mediumHandler); mediumHandler.setNext(highHandler); } @Test void testLowSeverityHandling() { Request request = new Request("BUG", "Minor issue", 2); // Capture output or use mocking framework in real scenario assertDoesNotThrow(() -> lowHandler.handle(request)); } @Test void testMediumSeverityHandling() { Request request = new Request("FEATURE", "Medium priority", 5); assertDoesNotThrow(() -> lowHandler.handle(request)); } @Test void testHighSeverityHandling() { Request request = new Request("CRITICAL", "System down", 9); assertDoesNotThrow(() -> lowHandler.handle(request)); } @Test void testUnhandledRequest() { Request request = new Request("UNKNOWN", "Unknown type", 11); // Should not throw exception, but reach end of chain assertDoesNotThrow(() -> lowHandler.handle(request)); } @Test void testChainOrder() { // Test that handlers are called in correct order MockHandler first = new MockHandler("First"); MockHandler second = new MockHandler("Second"); MockHandler third = new MockHandler("Third"); first.setNext(second); second.setNext(third); Request request = new Request("TEST", "Test request", 1); first.handle(request); // Verify all handlers were called assertTrue(first.wasCalled()); assertTrue(second.wasCalled()); assertTrue(third.wasCalled()); } } // Mock handler for testing class MockHandler implements Handler { private String name; private boolean called = false; private Handler next; public MockHandler(String name) { this.name = name; } @Override public void setNext(Handler handler) { this.next = handler; } @Override public void handle(Request request) { this.called = true; System.out.println(name + " handler called"); if (next != null) { next.handle(request); } } public boolean wasCalled() { return called; } } Best Practices and Considerations
1. When to Use Chain of Responsibility
// Good use cases: // 1. Multiple objects may handle a request public class PurchaseApproval { // Different managers can approve different amounts } // 2. You want to decouple sender and receiver public class EventSystem { // Events can be handled by multiple subscribers } // 3. The handler isn't known in advance public class PluginSystem { // Plugins can register handlers dynamically } // 4. You need dynamic handler configuration public class FilterChain { // Filters can be added/removed at runtime } 2. When NOT to Use Chain of Responsibility
// Avoid when: // 1. Only one handler should process each request public class SingleHandlerSystem { // Use Command or Strategy pattern instead } // 2. The chain order is fixed and known public class FixedPipeline { // Consider Template Method pattern } // 3. Performance is critical and chain is long public class PerformanceCriticalSystem { // Long chains can impact performance } 3. Combining with Other Patterns
// Chain of Responsibility + Composite public class CompositeHandler implements Handler { private List<Handler> children = new ArrayList<>(); public void addHandler(Handler handler) { children.add(handler); } @Override public void setNext(Handler handler) { // Composite pattern: set next for all children for (Handler child : children) { child.setNext(handler); } } @Override public void handle(Request request) { for (Handler handler : children) { handler.handle(request); } } } // Chain of Responsibility + Observer public class ObservableHandler extends AbstractHandler { private List<HandlerObserver> observers = new ArrayList<>(); public void addObserver(HandlerObserver observer) { observers.add(observer); } @Override public void handle(Request request) { notifyObservers(request, "BEFORE_PROCESSING"); super.handle(request); notifyObservers(request, "AFTER_PROCESSING"); } private void notifyObservers(Request request, String event) { for (HandlerObserver observer : observers) { observer.onHandlerEvent(this, request, event); } } } interface HandlerObserver { void onHandlerEvent(Handler handler, Request request, String event); } Common Pitfalls and Solutions
1. Infinite Chain Loops
// Problem: Circular reference in chain Handler handler1 = new ConcreteHandler(); Handler handler2 = new ConcreteHandler(); handler1.setNext(handler2); handler2.setNext(handler1); // Circular reference! // Solution: Validate chain setup public class SafeHandlerChainBuilder { private Set<Handler> handlers = new HashSet<>(); public SafeHandlerChainBuilder addHandler(Handler handler) { if (handlers.contains(handler)) { throw new IllegalArgumentException("Circular reference detected!"); } handlers.add(handler); // ... rest of builder logic return this; } } 2. Performance Issues with Long Chains
// Problem: Long chains can be slow // Solution: Use caching or short-circuiting public class CachingHandler extends AbstractHandler { private Map<Request, AuthResult> cache = new HashMap<>(); @Override public void handle(Request request) { if (cache.containsKey(request)) { // Use cached result AuthResult cached = cache.get(request); System.out.println("Using cached result: " + cached.getMessage()); return; } // Process and cache result super.handle(request); cache.put(request, new AuthResult(true, true, "Cached", "USER")); } } 3. Debugging Complex Chains
// Problem: Hard to debug which handler processed request // Solution: Add logging and tracing public class TraceableHandler extends AbstractHandler { private String name; public TraceableHandler(String name) { this.name = name; } @Override public void handle(Request request) { System.out.println("[" + name + "] Processing request: " + request); if (canHandle(request)) { System.out.println("[" + name + "] Can handle this request"); process(request); } else { System.out.println("[" + name + "] Passing to next handler"); super.handle(request); } System.out.println("[" + name + "] Finished processing"); } @Override protected boolean canHandle(Request request) { // Implementation return true; } @Override protected void process(Request request) { // Implementation } } Conclusion
The Chain of Responsibility Pattern is a powerful behavioral pattern that:
- Decouples senders and receivers of requests
- Provides flexibility in assigning responsibilities
- Supports dynamic handler configuration
- Enables open/closed principle - easy to add new handlers
Key Benefits:
- Reduced coupling between sender and receiver
- Increased flexibility in assigning responsibilities
- Dynamic configuration of handlers at runtime
- Single responsibility - each handler has one purpose
- Open/closed principle - easy to extend with new handlers
Use When:
- Multiple objects may handle a request
- You want to decouple the sender from specific receivers
- The handler isn't known in advance
- You need dynamic handler configuration
The Chain of Responsibility Pattern is widely used in frameworks like:
- Servlet Filters in Java EE
- Spring Security filter chains
- Logging frameworks
- Event processing systems
- Middleware pipelines
It provides an elegant solution for building flexible and maintainable processing pipelines in Java applications.