Fundamentals 24 min read

Chain of Responsibility in Kotlin: Real‑World Analogy, Benefits, and Full Example

Explore the Chain of Responsibility design pattern, its definition, real‑world doctor analogy, when and why to use it, advantages and drawbacks, and a complete Kotlin implementation that demonstrates dynamic request routing, extensibility, and practical code output.

DevOps Coach
DevOps Coach
DevOps Coach
Chain of Responsibility in Kotlin: Real‑World Analogy, Benefits, and Full Example

What is the Chain of Responsibility pattern?

The Chain of Responsibility (CoR) is a behavioral design pattern where a request is passed along a chain of potential handlers until one of them processes it. The client only knows the first handler, keeping the sender decoupled from concrete receivers and adhering to the Open/Closed Principle.

Real‑world analogy

Imagine a patient seeking medical advice. The patient (the request) first sees a general practitioner (GP). If the GP cannot treat the condition, they refer the patient to a specialist (ENT, cardiologist, surgeon, etc.). The specialist may further refer the patient until someone can resolve the issue, or the chain ends with no solution. This mirrors how CoR routes a request through handlers.

How it works

Typically you define a common interface or abstract class for all handlers, containing a method such as handleRequest and a reference to the next handler. Each concrete handler decides whether it can process the request; if not, it forwards the request to the next handler. The chain stops when a handler processes the request or when the end of the chain is reached.

When and why to use it

Dynamic request routing: Useful when a request may be handled by multiple objects and the concrete handler can only be determined at runtime.

Avoid large conditional logic: Replaces long if/else if or switch chains with a series of independent handler classes, improving readability and maintainability.

Flexible and extensible chain: Handlers can be added, removed, or reordered without changing client code, supporting the Open/Closed Principle.

Typical use cases

UI or game engine event handling (e.g., mouse click bubbles up widget hierarchy).

Logging systems with multiple log handlers (console, file, remote server).

Technical support escalation (Tier 1 → Tier 2 → Tier 3).

Approval workflows where a request climbs management levels until authorized.

Kotlin implementation

Below is a complete Kotlin example that models a help‑desk support escalation chain.

// The base Handler interface
interface SupportHandler {
    fun handleRequest(issue: SupportIssue)
}

// Simple data class representing a support issue
data class SupportIssue(val description: String, val complexity: Int)

An abstract base class implements the chain logic and defines two abstract methods that concrete handlers must provide.

abstract class AbstractSupportHandler(private val next: SupportHandler? = null) : SupportHandler {
    abstract fun canHandle(issue: SupportIssue): Boolean
    abstract fun doHandle(issue: SupportIssue)

    override fun handleRequest(issue: SupportIssue) {
        if (canHandle(issue)) {
            doHandle(issue)
        } else {
            if (next != null) {
                println("${this::class.simpleName} can't handle (complexity=${issue.complexity}). Passing to ${next::class.simpleName}...")
                next.handleRequest(issue)
            } else {
                println("Issue '${issue.description}' could not be handled by anyone in the chain.")
            }
        }
    }
}

Concrete handlers for three support tiers:

class Tier1Support(next: SupportHandler? = null) : AbstractSupportHandler(next) {
    override fun canHandle(issue: SupportIssue) = issue.complexity <= 1
    override fun doHandle(issue: SupportIssue) {
        println("Tier1Support: Resolved issue '${issue.description}'.")
    }
}

class Tier2Support(next: SupportHandler? = null) : AbstractSupportHandler(next) {
    override fun canHandle(issue: SupportIssue) = issue.complexity <= 2
    override fun doHandle(issue: SupportIssue) {
        println("Tier2Support: Resolved issue '${issue.description}'.")
    }
}

class Tier3Support(next: SupportHandler? = null) : AbstractSupportHandler(next) {
    override fun canHandle(issue: SupportIssue) = issue.complexity <= 3
    override fun doHandle(issue: SupportIssue) {
        println("Tier3Support: Resolved issue '${issue.description}'.")
    }
}

Setting up the chain and testing it with issues of varying complexity:

fun main() {
    val tier3 = Tier3Support()
    val tier2 = Tier2Support(next = tier3)
    val tier1 = Tier1Support(next = tier2)
    val supportChain: SupportHandler = tier1

    val simpleIssue = SupportIssue("Forgotten password", complexity = 1)
    val intermediateIssue = SupportIssue("System running slow", complexity = 2)
    val hardIssue = SupportIssue("Data loss in database", complexity = 3)
    val impossibleIssue = SupportIssue("Quantum server anomaly", complexity = 5)

    supportChain.handleRequest(simpleIssue)
    supportChain.handleRequest(intermediateIssue)
    supportChain.handleRequest(hardIssue)
    supportChain.handleRequest(impossibleIssue)
}

Sample output:

Tier1Support: Resolved issue 'Forgotten password'.
Tier1Support can't handle (complexity=2). Passing to Tier2Support...
Tier2Support: Resolved issue 'System running slow'.
Tier1Support can't handle (complexity=3). Passing to Tier2Support...
Tier2Support can't handle (complexity=3). Passing to Tier3Support...
Tier3Support: Resolved issue 'Data loss in database'.
Tier1Support can't handle (complexity=5). Passing to Tier2Support...
Tier2Support can't handle (complexity=5). Passing to Tier3Support...
Issue 'Quantum server anomaly' could not be handled by anyone in the chain.

Advantages

Reduced coupling: The sender knows only the first handler.

Flexibility and extensibility: New handlers can be added or reordered without touching client code.

Dynamic control flow: Handlers can be assembled at runtime based on configuration.

Single responsibility: Each handler focuses on a specific condition.

Default handling: A fallback handler can catch unprocessed requests.

Reusability: Handlers are independent classes that can be reused in different chains.

Drawbacks and limitations

Uncertain handling: Without a final default handler, a request may disappear.

Debugging difficulty: Tracing the path of a request through a long or dynamically built chain can be hard.

Performance overhead: Each hop adds a method call (and possibly a network hop in distributed setups).

Potential over‑engineering: For simple two‑branch decisions, a plain if/else may be clearer.

Order sensitivity: Placing a generic handler before a specific one can mask the latter.

Hidden coupling to the chain: Handlers must know about the "next" reference, introducing some boilerplate.

Conclusion

The Chain of Responsibility pattern provides a clean, decoupled way to handle requests that may have multiple potential processors. It replaces brittle conditional logic with a flexible object‑oriented chain, improves maintainability, and aligns with core design principles such as decoupling, single responsibility, and the Open/Closed Principle. Use it when you have several interchangeable handlers, dynamic routing needs, or configurable processing pipelines, but avoid it for trivial cases where a simple conditional would suffice.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Chain of ResponsibilitySoftware ArchitectureKotlindesign patternObject-Oriented
DevOps Coach
Written by

DevOps Coach

Master DevOps precisely and progressively.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.