Chain of Responsibility Design Pattern in Kotlin: A Detailed Guide

Table of Contents

Design patterns are a cornerstone of writing clean, maintainable, and reusable code. One of the more elegant patterns, the Chain of Responsibility (CoR), allows us to build a flexible system where multiple handlers can process a request in a loosely coupled manner. Today, we’ll dive deep into how this design pattern works and how to implement it in Kotlin.

What is the Chain of Responsibility Pattern?

The Chain of Responsibility design pattern is a behavioral design pattern that allows passing a request along a chain of handlers, where each handler has a chance to process the request or pass it along to the next handler in the chain. The main goal is to decouple the sender of a request from its receivers, giving multiple objects a chance to handle the request.

That means the CoR pattern allows multiple objects to handle a request without the sender needing to know which object handled it. The request is passed along a chain of objects (handlers), where each handler has the opportunity to process it or pass it to the next one.

Think of a company where a request, such as budget approval, must go through several levels of management. At each level, the manager can either address the request or escalate it to the next level.

Now imagine another situation: an employee submits a leave application. Depending on the duration of leave, it might need approval from different authorities, such as a team leader, department head, or higher management.

These scenarios capture the essence of the Chain of Responsibility design pattern, where a request is passed along a series of handlers, each with the choice to process it or forward it.

Why Use the Chain of Responsibility Pattern?

Before we delve into the structure and implementation of the Chain of Responsibility (CoR) pattern, let’s first understand why it’s important.

Consider a situation where multiple objects are involved in processing a request, and the handling varies depending on the specific conditions. For example, in online shopping platforms like Myntra or Amazon, or food delivery services such as Zomato or Swiggy, a customer might use a discount code or coupon. The system needs to determine if the code is valid or decide which discount should apply based on the circumstances.

This is where the Chain of Responsibility pattern becomes highly useful. Rather than linking the request to a specific handler, it enables the creation of a chain of handlers, each capable of managing the request in its unique way. This makes the system more adaptable, allowing developers to easily add, remove, or modify handlers without affecting the core logic.

So, the Chain of Responsibility pattern offers several advantages:

  • Decouples the sender and receiver: The sender doesn’t need to know which object in the chain will handle the request.
  • Simplifies the code: It eliminates complex conditionals and decision trees by delegating responsibility to handlers in the chain.
  • Adds flexibility: New handlers can be seamlessly added to the chain without impacting the existing implementation.

We’ll look at the actual code and explore additional real-world examples shortly to make this concept even clearer.

Structure of the Chain of Responsibility Pattern

The Chain of Responsibility pattern consists of:

  1. Handler Interface: Declares a method to process requests and optionally set the next handler.
  2. Concrete Handlers: Implements the interface and processes the request.
  3. Client Code: Creates and configures the chain.
Structure of the Chain of Responsibility

Handler (Abstract Class or Interface)

Defines the interface for handling requests and the reference to the next handler in the chain.

Kotlin
abstract class Handler {
    protected var nextHandler: Handler? = null
    
    abstract fun handleRequest(request: String)
    
    fun setNextHandler(handler: Handler) {
        nextHandler = handler
    }
}
  • This defines an interface for handling requests, usually with a method like handleRequest(). It may also have a reference to the next handler in the chain.
  • The handler may choose to process the request or pass it on to the next handler.

ConcreteHandler

Implement the handleRequest() method to either process the request or pass it to the next handler.

Kotlin
class ConcreteHandlerA : Handler() {
    override fun handleRequest(request: String) {
        if (request == "A") {
            println("Handler A processed request: $request")
        } else {
            nextHandler?.handleRequest(request)
        }
    }
}

class ConcreteHandlerB : Handler() {
    override fun handleRequest(request: String) {
        if (request == "B") {
            println("Handler B processed request: $request")
        } else {
            nextHandler?.handleRequest(request)
        }
    }
}
  • These are the actual handler classes that implement the handleRequest() method. Each concrete handler will either process the request or pass it to the next handler in the chain.
  • If a handler is capable of processing the request, it does so; otherwise, it forwards the request to the next handler in the chain.

Client

Interacts only with the first handler in the chain, unaware of the specific handler processing the request.

Kotlin
fun main() {
    val handlerA = ConcreteHandlerA()
    val handlerB = ConcreteHandlerB()

    handlerA.setNextHandler(handlerB)
    
    // Client sends the request to the first handler
    handlerA.handleRequest("A") // Handler A processes the request
    handlerA.handleRequest("B") // Handler B processes the request
}
  • The client sends the request to the first handler in the chain. The client does not need to know which handler will eventually process the request.

As we can see, this structure allows requests to pass through multiple handlers in the chain, with each handler having the option to process the request or delegate it.

Now, let’s roll up our sleeves and dive into real-world use case code.

Real-World Use Case

Now, let’s roll up our sleeves and dive into real-world use case code.

Handling Employee Request

Let’s revisit our employee leave request scenario, where we need to approve a leave request in a company. The leave request should be processed by different authorities depending on the amount of leave being requested. Here’s the hierarchy:

CoR in Leave Request
  • Employee: Initiates the leave request by submitting it to their immediate supervisor or system.
  • Manager (up to 5 days): Approves short leaves to handle minor requests efficiently.
  • Director (up to 15 days): Approves extended leaves, ensuring alignment with organizational policies.
  • HR (more than 15 days): Handles long-term leave requests, requiring policy compliance or special considerations.

Using the Chain of Responsibility, we can chain the approval process such that if one handler (e.g., Manager) cannot process the request, it is passed to the next handler (e.g., Director).

Define the Handler Interface

The handler interface is a blueprint for the handlers that will process requests. Each handler can either process the request or pass it along to the next handler in the chain.

Kotlin
// Create the Handler interface
interface LeaveRequestHandler {
    fun handleRequest(request: LeaveRequest)
}

In this case, the handleRequest function takes a LeaveRequest object, which holds the details of the leave request, and processes it.

Define the Request Object

The request object contains all the information related to the request. Here, we’ll create a simple LeaveRequest class.

Kotlin
// Create the LeaveRequest object
data class LeaveRequest(val employeeName: String, val numberOfDays: Int)

Create Concrete Handlers

Now, we’ll implement different concrete handlers for each authority: Manager, Director, and HR. Each handler will check if it can approve the leave request based on the number of days requested.

Kotlin
// Implement the Manager Handler
class Manager(private val nextHandler: LeaveRequestHandler? = null) : LeaveRequestHandler {
    override fun handleRequest(request: LeaveRequest) {
        if (request.numberOfDays <= 5) {
            println("Manager approved ${request.employeeName}'s leave for ${request.numberOfDays} days.")
        } else {
            nextHandler?.handleRequest(request)
        }
    }
}

// Implement the Director Handler
class Director(private val nextHandler: LeaveRequestHandler? = null) : LeaveRequestHandler {
    override fun handleRequest(request: LeaveRequest) {
        if (request.numberOfDays <= 15) {
            println("Director approved ${request.employeeName}'s leave for ${request.numberOfDays} days.")
        } else {
            nextHandler?.handleRequest(request)
        }
    }
}

// Implement the HR Handler
class HR : LeaveRequestHandler {
    override fun handleRequest(request: LeaveRequest) {
        println("HR approved ${request.employeeName}'s leave for ${request.numberOfDays} days.")
    }
}

Each handler checks whether the request can be processed. If it cannot, the request is passed to the next handler in the chain.

Set Up the Chain

Next, we’ll set up the chain of responsibility. We will link the handlers so that each handler knows who to pass the request to if it can’t handle it.

Kotlin
// Setup the chain
fun createLeaveApprovalChain(): LeaveRequestHandler {
    val hr = HR()
    val director = Director(hr)
    val manager = Manager(director)
    
    return manager // The chain starts with the Manager
}
  • If the Manager can’t approve the leave (i.e., the request is for more than 5 days), it passes the request to the Director.
  • If the Director can’t approve the leave (i.e., the request is for more than 15 days), it passes the request to HR, which will handle it.

Test the Chain of Responsibility

Now, let’s create a LeaveRequest and pass it through the chain.

Kotlin
fun main() {
    val leaveRequest = LeaveRequest("amol pawar", 10)
    
    val approvalChain = createLeaveApprovalChain()
    approvalChain.handleRequest(leaveRequest)
}

// OUTPUT

// Director approved amol pawar's leave for 10 days.

Now, let’s explore an E-Commerce Discount System using CoR.

Discount Handling in an E-Commerce System

Let’s imagine a scenario with platforms like Myntra, Flipkart, or Amazon, where we have different types of discounts:

  • Coupon Discount (10% off)
  • Member Discount (5% off)
  • Seasonal Discount (15% off)

In this case, we’ll pass a request for a discount through a chain of handlers. Each handler checks if it can process the request or passes it to the next one in the chain.

Let’s now see how we can implement the Chain of Responsibility pattern in this case.

Define the Handler Interface

The handler interface will define a method handleRequest() that every concrete handler will implement.

Kotlin
// Handler Interface
interface DiscountHandler {
    fun setNext(handler: DiscountHandler): DiscountHandler
    fun handleRequest(amount: Double): Double
}

Here, the setNext() method will allow us to chain multiple handlers together, and handleRequest() will process the request.

Concrete Handlers

Let’s define the different discount handlers. Each one will check if it can handle the discount request. If it can’t, it will forward it to the next handler.

Kotlin
// Concrete Handler for Coupon Discount
class CouponDiscountHandler(private val discount: Double) : DiscountHandler {
    private var nextHandler: DiscountHandler? = null

    override fun setNext(handler: DiscountHandler): DiscountHandler {
        nextHandler = handler
        return handler
    }

    override fun handleRequest(amount: Double): Double {
        val discountedAmount = if (discount > 0) {
            println("Applying Coupon Discount: $discount%")
            amount - (amount * (discount / 100))
        } else {
            nextHandler?.handleRequest(amount) ?: amount
        }
        return discountedAmount
    }
}

// Concrete Handler for Member Discount
class MemberDiscountHandler(private val discount: Double) : DiscountHandler {
    private var nextHandler: DiscountHandler? = null

    override fun setNext(handler: DiscountHandler): DiscountHandler {
        nextHandler = handler
        return handler
    }

    override fun handleRequest(amount: Double): Double {
        val discountedAmount = if (discount > 0) {
            println("Applying Member Discount: $discount%")
            amount - (amount * (discount / 100))
        } else {
            nextHandler?.handleRequest(amount) ?: amount
        }
        return discountedAmount
    }
}

// Concrete Handler for Seasonal Discount
class SeasonalDiscountHandler(private val discount: Double) : DiscountHandler {
    private var nextHandler: DiscountHandler? = null

    override fun setNext(handler: DiscountHandler): DiscountHandler {
        nextHandler = handler
        return handler
    }

    override fun handleRequest(amount: Double): Double {
        val discountedAmount = if (discount > 0) {
            println("Applying Seasonal Discount: $discount%")
            amount - (amount * (discount / 100))
        } else {
            nextHandler?.handleRequest(amount) ?: amount
        }
        return discountedAmount
    }
}

Client Code

Now that we have defined our concrete handlers, let’s see how the client can set up the chain and make a request.

Kotlin
fun main() {
    // Creating discount handlers
    val couponHandler = CouponDiscountHandler(10.0) // 10% off
    val memberHandler = MemberDiscountHandler(5.0) // 5% off
    val seasonalHandler = SeasonalDiscountHandler(15.0) // 15% off

    // Chain the handlers
    couponHandler.setNext(memberHandler).setNext(seasonalHandler)

    // Client request
    val originalAmount = 1000.0
    val finalAmount = couponHandler.handleRequest(originalAmount)

    println("Final Amount after applying discounts: $finalAmount")
}

// OUTPUT

// Applying Coupon Discount: 10.0%
// Final Amount after applying discounts: 900.0

Here,

  • Handlers: We define three concrete handlers, each responsible for applying a specific discount: coupon, member, and seasonal.
  • Chaining: We chain the handlers together using setNext(). The chain is set up such that if one handler can’t process the request, it passes the request to the next handler.
  • Request Processing: The client sends a request for the original amount (1000.0 in our example) to the first handler (couponHandler). If the handler can process the request, it applies the discount and returns the discounted amount. If it can’t, it forwards the request to the next handler.

But here’s the twist: what if we want to apply only the member discount? What will the output be, and how can we achieve this? Let’s see how we can do it. To achieve this, we need to set all other discounts to either 0 or negative. Only then will we get the exact output we want. 

Here, I’ll skip the actual code and just show the changes and the output.

Kotlin
    val couponHandler = CouponDiscountHandler(0.0) 
    val memberHandler = MemberDiscountHandler(5.0) // only applying 5% off
    val seasonalHandler = SeasonalDiscountHandler(-5.0) // 0 or negative 

    
    // Output 

    // Applying Member Discount: 5.0%
    // Final Amount after applying discounts: 950.0

Please note that this might seem a bit confusing, so let me clarify. In this example, we apply only one discount at a time. To achieve this, we set the other discounts to zero, ensuring that only the selected discount is applied. This is because we’ve set the starting handler to the coupon discount, which prevents the other discounts from being applied sequentially. As we often see on platforms like Flipkart and Amazon, only one coupon can typically be applied at a time.

However, this doesn’t mean that applying multiple coupons sequentially (i.e., applying all selected discounts at once) is impossible using the Chain of Responsibility (CoR) pattern. In fact, it can be achieved through the accumulation of discounts, where the total amount is updated after each sequential discount (e.g., coupon, member discount, seasonal discount). This allows us to apply all the discounts and calculate the final amount.

The sequential discount approach is particularly useful in the early stages of a business, when the goal is to attract more users or customers. In well-established e-commerce systems, however, only one discount is usually applied. In our case, we achieve this by setting the other discounts to either zero or a negative value.

Just to clarify, this explanation is a simplified scenario to help understand the concept. Real-world use cases often involve more complexities and variations. I hope this helps clear things up..!

Now, one more familiar use case that many of us have likely worked with in past projects is the Logging System.

Logging System

Let’s implement a logging system where log messages are passed through a chain of loggers. Each logger decides whether to handle the log or pass it along. We’ll include log levels: INFO, DEBUG, and ERROR.

Define a Base Logger Class

First, we’ll create an abstract Logger class. This will act as the base for all specific loggers.

Kotlin
abstract class Logger(private val level: Int) {

    private var nextLogger: Logger? = null

    // Set the next logger in the chain
    fun setNext(logger: Logger): Logger {
        this.nextLogger = logger
        return logger
    }

    // Handle the log request
    fun logMessage(level: Int, message: String) {
        if (this.level == level) {
            write(message)
            return                // To stop further propagation
        }
        nextLogger?.logMessage(level, message)
    }

    // Abstract method for handling the log
    protected abstract fun write(message: String)
}

Here,

  • level: Defines the log level this logger will handle.
  • nextLogger: Points to the next handler in the chain.
  • setNext: Configures the next logger in the chain, enabling chaining.
  • logMessage: Checks if the logger should process the message based on its level. If not, it delegates the task to the next logger in the chain.
  • return: Why used return here..? The return is used to prevent the log message from being passed to the next logger after it has been processed. It ensures that once a logger handles the message, it won’t be forwarded further, avoiding redundant output.

Create Specific Logger Implementations

Let’s create concrete loggers for INFO, DEBUG, and ERROR levels.

Kotlin
class InfoLogger : Logger(1) {
    override fun write(message: String) {
        println("INFO: $message")
    }
}

class DebugLogger : Logger(2) {
    override fun write(message: String) {
        println("DEBUG: $message")
    }
}

class ErrorLogger : Logger(3) {
    override fun write(message: String) {
        println("ERROR: $message")
    }
}
  • Each logger overrides the write method to handle messages at its respective log level.
  • For instance, the InfoLogger handles logs with a level of 1, while higher levels (DEBUG or ERROR) are passed down the chain.

Setting Up the Chain

Now, let’s set up a chain of loggers: INFO → DEBUG → ERROR.

Kotlin
fun getLoggerChain(): Logger {
    val errorLogger = ErrorLogger()
    val debugLogger = DebugLogger()
    val infoLogger = InfoLogger()

    infoLogger.setNext(debugLogger).setNext(errorLogger)

    return infoLogger
}
  • We instantiate the loggers and chain them together using setNext.
  • The chain starts with InfoLogger and ends with ErrorLogger.

Using the Logger Chain

Now, let’s test our chain.

Kotlin
fun main() {
    val loggerChain = getLoggerChain()

    println("Testing Chain of Responsibility:")
    loggerChain.logMessage(1, "This is an info message.")
    loggerChain.logMessage(2, "This is a debug message.")
    loggerChain.logMessage(3, "This is an error message.")
}

Output

Kotlin
Testing Chain of Responsibility:
INFO: This is an info message.
DEBUG: This is a debug message.
ERROR: This is an error message.

Basically, what happens here is,

  • A log with level 1 is processed by InfoLogger.
  • A log with level 2 is handled by DebugLogger.
  • A log with level 3 is processed by ErrorLogger.

If no logger in the chain can handle a request, it simply passes through without being processed.

Extending the Chain

What if we wanted to add more log levels, like TRACE? Easy..! Just implement a TraceLogger and link it to the chain without modifying the existing code.

Kotlin
class TraceLogger : Logger(0) {
    override fun write(message: String) {
        println("TRACE: $message")
    }
}

We also need to adjust the chain setup.

Kotlin
val traceLogger = TraceLogger()
traceLogger.setNext(infoLogger)

A Few More Real-Life Applications

  • Middleware in Web Frameworks: Processing HTTP requests in frameworks like Spring Boot or Ktor.
  • Authorization Systems: Sequential permission checks.
  • Form Validation: Sequential checks for input validation.
  • Payment Processing: Delegating payment methods to appropriate processors.

Advantages and Disadvantages

Advantages

  • Flexibility: Easily add or remove handlers in the chain.
  • Decoupling: The sender doesn’t know which handler processes the request.
  • Open/Closed Principle: New handlers can be added without modifying existing code.

Disadvantages

  • No Guarantee of Handling: If no handler processes the request, it may go unhandled.
  • Debugging Complexity: Long chains can be hard to trace.

Conclusion

The Chain of Responsibility pattern offers an elegant solution for delegating requests through a sequence of handlers. By decoupling the sender of a request from its receivers, this approach enhances flexibility and improves the maintainability of your code. Each handler focuses on specific tasks and passes unhandled requests to the next handler in the chain.

In this guide, we explored practical examples to illustrate the use of the Chain of Responsibility pattern in Kotlin, showcasing its application in scenarios like leave approval and discount processing. The pattern proves highly adaptable to diverse use cases, allowing for clean and organized request handling.

Incorporating this pattern into your design helps build systems that are easier to extend, maintain, and scale, ensuring they remain robust in the face of changing requirements.

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!