Design Pattern 18: Chain of Responsibility Pattern - Complete Guide with Real-World Logging Examples
π Download the complete Design Pattern series code from our design_pattern repository.
π― What is the Chain of Responsibility Pattern?
The Chain of Responsibility Pattern is a behavioral design pattern that allows you to 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. This pattern promotes loose coupling and provides flexibility in handling requests.
Key Benefits:
- β Loose Coupling - Handlers are independent and can be easily modified
- β Flexibility - Dynamic chain composition and reordering
- β Single Responsibility - Each handler has a specific responsibility
- β Extensibility - Easy to add new handlers without changing existing code
- β Request Processing - Multiple handlers can process the same request
π Real-World Problem: Multi-Level Logging System
Letβs design a multi-level logging system with the following requirements:
System Requirements:
- Support multiple log levels (INFO, WARNING, ERROR, DEBUG)
- Dynamic handler chain - handlers can be added/removed at runtime
- Independent handlers - each handler processes specific log levels
- Extensibility - easy to add new logging destinations (Console, File, Database, Email)
- Performance optimization - efficient processing for high-volume logging
Business Rules:
- Each log level should be handled by appropriate handlers
- Handlers should be able to process multiple requests in sequence
- System should support handler ordering and priority
- Failed handlers should not break the entire chain
- Support for conditional processing based on log content
ποΈ Object-Oriented Analysis (OOA)
Letβs analyze the problem and identify the core components:

Identified Forces:
- High Coupling
- Client code directly controls each logging handler
- Tight coupling between client and handler implementations
- Lack of Flexibility
- Cannot easily adjust handler order or add new handlers
- Static handler configuration limits system adaptability
- Violation of Open-Closed Principle (OCP)
- Adding new logging handlers requires client code changes
- System becomes rigid and hard to extend
π‘ Chain of Responsibility Pattern Solution
After analyzing the forces, we can apply the Chain of Responsibility Pattern to create a flexible request processing system:

Chain of Responsibility Pattern Components:
- Handler Interface - Defines the interface for handling requests
- Concrete Handlers - Implement specific handling logic
- Chain Builder - Constructs and manages the handler chain
- Client - Initiates requests without knowing chain details
Benefits:
- Dynamic chain composition - Handlers can be added/removed at runtime
- Loose coupling - Client doesnβt need to know about specific handlers
- Flexible processing - Multiple handlers can process the same request
- Easy extension - Add new handlers without changing existing code
π οΈ Implementation: Multi-Level Logging System

1. Handler Interface
abstract class Logger(
private val nextLogger: Logger? = null
) {
abstract fun log(level: LogLevel, message: String)
protected fun passToNext(level: LogLevel, message: String) {
nextLogger?.log(level, message)
}
}
enum class LogLevel {
DEBUG, INFO, WARNING, ERROR
}
data class LogRequest(
val level: LogLevel,
val message: String,
val timestamp: Long = System.currentTimeMillis(),
val source: String = "Application"
)
2. Concrete Handlers
class ConsoleLogger(
nextLogger: Logger? = null,
private val minLevel: LogLevel = LogLevel.INFO
) : Logger(nextLogger) {
override fun log(level: LogLevel, message: String) {
if (level.ordinal >= minLevel.ordinal) {
val timestamp = java.time.LocalDateTime.now().format(
java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
)
println("πΊ [${level.name}] $timestamp - $message")
}
passToNext(level, message)
}
}
class FileLogger(
nextLogger: Logger? = null,
private val filename: String = "application.log",
private val minLevel: LogLevel = LogLevel.WARNING
) : Logger(nextLogger) {
override fun log(level: LogLevel, message: String) {
if (level.ordinal >= minLevel.ordinal) {
val timestamp = java.time.LocalDateTime.now().format(
java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
)
val logEntry = "[${level.name}] $timestamp - $message\n"
try {
java.io.File(filename).appendText(logEntry)
println("π FileLogger: Logged to $filename")
} catch (e: Exception) {
println("β FileLogger: Failed to write to $filename - ${e.message}")
}
}
passToNext(level, message)
}
}
class DatabaseLogger(
nextLogger: Logger? = null,
private val minLevel: LogLevel = LogLevel.ERROR
) : Logger(nextLogger) {
override fun log(level: LogLevel, message: String) {
if (level.ordinal >= minLevel.ordinal) {
val timestamp = java.time.LocalDateTime.now().format(
java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
)
// Simulate database insertion
println("ποΈ DatabaseLogger: INSERT INTO logs (level, message, timestamp) VALUES ('${level.name}', '$message', '$timestamp')")
}
passToNext(level, message)
}
}
class EmailLogger(
nextLogger: Logger? = null,
private val adminEmail: String = "admin@company.com",
private val minLevel: LogLevel = LogLevel.ERROR
) : Logger(nextLogger) {
override fun log(level: LogLevel, message: String) {
if (level.ordinal >= minLevel.ordinal) {
val timestamp = java.time.LocalDateTime.now().format(
java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
)
// Simulate email sending
println("π§ EmailLogger: Sending email to $adminEmail - Subject: [${level.name}] $timestamp - $message")
}
passToNext(level, message)
}
}
3. Chain Builder
class LoggingChainBuilder {
private var firstLogger: Logger? = null
private var currentLogger: Logger? = null
fun addConsoleLogger(minLevel: LogLevel = LogLevel.INFO): LoggingChainBuilder {
val logger = ConsoleLogger(null, minLevel)
addLogger(logger)
return this
}
fun addFileLogger(filename: String = "application.log", minLevel: LogLevel = LogLevel.WARNING): LoggingChainBuilder {
val logger = FileLogger(null, filename, minLevel)
addLogger(logger)
return this
}
fun addDatabaseLogger(minLevel: LogLevel = LogLevel.ERROR): LoggingChainBuilder {
val logger = DatabaseLogger(null, minLevel)
addLogger(logger)
return this
}
fun addEmailLogger(adminEmail: String = "admin@company.com", minLevel: LogLevel = LogLevel.ERROR): LoggingChainBuilder {
val logger = EmailLogger(null, adminEmail, minLevel)
addLogger(logger)
return this
}
private fun addLogger(logger: Logger) {
if (firstLogger == null) {
firstLogger = logger
currentLogger = logger
} else {
// Find the last logger in the chain
var lastLogger = firstLogger
while (lastLogger?.let { it::class.java.getDeclaredField("nextLogger").apply { isAccessible = true }.get(it) } != null) {
lastLogger = lastLogger::class.java.getDeclaredField("nextLogger").apply { isAccessible = true }.get(lastLogger) as Logger?
}
// Set the next logger
lastLogger?.let {
it::class.java.getDeclaredField("nextLogger").apply { isAccessible = true }.set(it, logger)
}
}
}
fun build(): Logger {
return firstLogger ?: throw IllegalStateException("No loggers added to chain")
}
}
4. Client Code
class LoggingManager(private val logger: Logger) {
fun logDebug(message: String) {
logger.log(LogLevel.DEBUG, message)
}
fun logInfo(message: String) {
logger.log(LogLevel.INFO, message)
}
fun logWarning(message: String) {
logger.log(LogLevel.WARNING, message)
}
fun logError(message: String) {
logger.log(LogLevel.ERROR, message)
}
}
fun main() {
// Build logging chain
val logger = LoggingChainBuilder()
.addConsoleLogger(LogLevel.INFO)
.addFileLogger("app.log", LogLevel.WARNING)
.addDatabaseLogger(LogLevel.ERROR)
.addEmailLogger("admin@company.com", LogLevel.ERROR)
.build()
val loggingManager = LoggingManager(logger)
println("=== Multi-Level Logging Demo ===\n")
// Test different log levels
loggingManager.logDebug("This is a debug message - only console should show")
println()
loggingManager.logInfo("This is an info message - console should show")
println()
loggingManager.logWarning("This is a warning message - console and file should show")
println()
loggingManager.logError("This is an error message - all handlers should show")
println()
// Test chain with different configuration
println("=== Custom Logging Chain Demo ===\n")
val customLogger = LoggingChainBuilder()
.addConsoleLogger(LogLevel.DEBUG)
.addEmailLogger("dev@company.com", LogLevel.WARNING)
.build()
val customManager = LoggingManager(customLogger)
customManager.logDebug("Debug message with custom chain")
customManager.logWarning("Warning message with custom chain")
}
Expected Output:
=== Multi-Level Logging Demo ===
πΊ [INFO] 2024-12-16 23:00:00 - This is an info message - console should show
πΊ [WARNING] 2024-12-16 23:00:01 - This is a warning message - console and file should show
π FileLogger: Logged to app.log
πΊ [ERROR] 2024-12-16 23:00:02 - This is an error message - all handlers should show
π FileLogger: Logged to app.log
ποΈ DatabaseLogger: INSERT INTO logs (level, message, timestamp) VALUES ('ERROR', 'This is an error message - all handlers should show', '2024-12-16 23:00:02')
π§ EmailLogger: Sending email to admin@company.com - Subject: [ERROR] 2024-12-16 23:00:02 - This is an error message - all handlers should show
=== Custom Logging Chain Demo ===
πΊ [DEBUG] 2024-12-16 23:00:03 - Debug message with custom chain
πΊ [WARNING] 2024-12-16 23:00:04 - Warning message with custom chain
π§ EmailLogger: Sending email to dev@company.com - Subject: [WARNING] 2024-12-16 23:00:04 - Warning message with custom chain
π Chain of Responsibility vs Alternative Approaches
Approach | Pros | Cons |
---|---|---|
Chain of Responsibility | β
Loose coupling β Dynamic chain composition β Easy extension | β Potential performance overhead β Chain traversal complexity β Debugging challenges |
Direct Handler Calls | β
Simple implementation β No overhead β Easy debugging | β Tight coupling β Hard to extend β Violates OCP |
Strategy Pattern | β
Runtime strategy switching β Clean separation | β Single handler per request β No chain processing |
Observer Pattern | β
Multiple observers β Decoupled communication | β No control over processing order β All observers process every event |
π― When to Use the Chain of Responsibility Pattern
β Perfect For:
- Request processing pipelines (web middleware, logging systems)
- Event handling systems (GUI event processing, game event systems)
- Validation chains (form validation, data processing)
- Error handling (exception handling chains)
- Authentication/Authorization (security middleware)
β Avoid When:
- Simple request handling (single handler sufficient)
- Performance-critical applications (chain traversal overhead)
- Fixed processing order (static handler configuration)
- Synchronous processing (blocking chain execution)
π§ Advanced Chain of Responsibility Implementations
1. Conditional Chain Processing
class ConditionalLogger(
nextLogger: Logger? = null,
private val condition: (LogLevel, String) -> Boolean
) : Logger(nextLogger) {
override fun log(level: LogLevel, message: String) {
if (condition(level, message)) {
println("π ConditionalLogger: Processing message that meets condition")
// Process the message
}
passToNext(level, message)
}
}
// Usage example
val conditionalLogger = ConditionalLogger(
condition = { level, message ->
level == LogLevel.ERROR && message.contains("database")
}
)
2. Chain with Priority
class PriorityLogger(
nextLogger: Logger? = null,
private val priority: Int = 0
) : Logger(nextLogger) {
override fun log(level: LogLevel, message: String) {
println("β‘ PriorityLogger (Priority: $priority): Processing message")
passToNext(level, message)
}
}
class PriorityChainBuilder {
private val handlers = mutableListOf<Pair<Int, Logger>>()
fun addHandler(priority: Int, logger: Logger): PriorityChainBuilder {
handlers.add(priority to logger)
return this
}
fun build(): Logger {
val sortedHandlers = handlers.sortedBy { it.first }
var currentLogger: Logger? = null
for (i in sortedHandlers.indices.reversed()) {
val (priority, logger) = sortedHandlers[i]
val nextLogger = currentLogger
currentLogger = object : Logger(nextLogger) {
override fun log(level: LogLevel, message: String) {
logger.log(level, message)
}
}
}
return currentLogger ?: throw IllegalStateException("No handlers added")
}
}
3. Chain with Error Handling
class ErrorHandlingLogger(
nextLogger: Logger? = null,
private val errorHandler: (Exception) -> Unit
) : Logger(nextLogger) {
override fun log(level: LogLevel, message: String) {
try {
println("π‘οΈ ErrorHandlingLogger: Processing message safely")
// Simulate potential error
if (message.contains("error")) {
throw RuntimeException("Simulated error in processing")
}
} catch (e: Exception) {
errorHandler(e)
println("β οΈ ErrorHandlingLogger: Error handled, continuing chain")
}
passToNext(level, message)
}
}
π Real-World Applications
1. Web Application Middleware
- Authentication middleware - Check user credentials
- Authorization middleware - Verify user permissions
- Logging middleware - Log request/response data
- Rate limiting middleware - Control request frequency
- CORS middleware - Handle cross-origin requests
2. GUI Event Processing
- Event bubbling - Process events up the component hierarchy
- Input validation - Validate user input at multiple levels
- Error handling - Handle errors at different UI layers
- Accessibility - Process accessibility events
3. Game Development
- Input processing - Handle user input through multiple systems
- Collision detection - Process collisions through different layers
- AI behavior - Process AI decisions through behavior trees
- Event systems - Handle game events through multiple listeners
4. Data Processing Pipelines
- ETL processes - Extract, transform, load data through stages
- Data validation - Validate data at multiple checkpoints
- Data transformation - Transform data through multiple processors
- Data routing - Route data to appropriate destinations
π Performance Considerations
Chain Traversal
- Chain length - Keep chains reasonably short for performance
- Early termination - Stop processing when appropriate
- Caching - Cache chain results for repeated requests
- Parallel processing - Process independent handlers in parallel
Memory Management
- Handler lifecycle - Manage handler creation and destruction
- Memory leaks - Avoid circular references in chains
- Resource cleanup - Properly clean up handler resources
- Object pooling - Reuse handler objects when possible
π Related Design Patterns
- Command Pattern - For encapsulating requests as objects
- Decorator Pattern - For adding behavior dynamically
- Observer Pattern - For event notification systems
- [Pipeline Pattern] - For processing data through multiple stages
π Best Practices
1. Chain Design
- Keep chains focused - Each chain should have a specific purpose
- Limit chain length - Avoid overly long chains for performance
- Handle failures gracefully - Ensure chain continues even if handlers fail
- Document chain behavior - Clear documentation of chain processing
2. Handler Implementation
- Single responsibility - Each handler should have one clear purpose
- Stateless handlers - Avoid storing state in handlers when possible
- Error handling - Proper error handling in each handler
- Performance optimization - Optimize handler performance
3. Chain Management
- Dynamic composition - Allow runtime chain modification
- Configuration management - Externalize chain configuration
- Monitoring and metrics - Track chain performance and usage
- Testing strategies - Test individual handlers and complete chains
π― Conclusion
The Chain of Responsibility Pattern provides a powerful way to process requests through multiple handlers while maintaining loose coupling and flexibility. By creating a chain of handlers, it enables:
- Flexible request processing with dynamic handler composition
- Loose coupling between clients and handlers
- Easy extension for new handlers without changing existing code
- Multiple processing paths for different types of requests
This pattern is essential for building robust, scalable systems that need to handle complex request processing workflows. Whether youβre building web applications, logging systems, or data processing pipelines, the Chain of Responsibility Pattern provides the foundation for flexible and maintainable request handling.
Next Steps:
- Explore the Command Pattern for encapsulating requests
- Learn about the Observer Pattern for event notification
- Discover the Decorator Pattern for dynamic behavior addition
Ready to implement the Chain of Responsibility Pattern in your projects? Download the complete code examples from our design_pattern repository and start building more flexible, maintainable systems today!
Enjoy Reading This Article?
Here are some more articles you might like to read next: