Greetings, traveler!
We continue to learn about Design Patterns. We have started to explore Behavioral Patterns, and the first one we will be looking at is the Chain of Responsibility Pattern.
If you ever had a piece of logic that looks like a growing list of conditions, you have probably seen this pattern without naming it.
A request comes in. Several components can potentially handle it. Each one checks whether the request matches its responsibility. If yes, it handles it. If not, it forwards the request to the next handler.
That is Chain of Responsibility — it is all about getting rid of giant switch blocks and nested if trees that turn into a maintenance trap the moment requirements change.
The problem: conditional logic that keeps expanding
Most applications have some form of state-driven processing:
- onboarding steps
- access restrictions
- password/security requirements
- “blocked” flows
- re-authentication
- subscription gates
It usually starts simple. Then somebody adds one more case. Then another. Then an exception. Then a special rule for a single screen. Soon you end up with one function that knows too much and changes too often.
This is where Chain of Responsibility fits well.
The Chain of responsibility is a design pattern that allows requests to be processed sequentially by a series of handlers. Each handler decides whether or not to process the request based on its own criteria. If a handler chooses to process the request, it may pass it on to the next handler in the chain or decide to handle it. This allows for flexibility in handling requests and ensures that each handler can choose whether or not it wants to take on the responsibility of processing the request.
What chain of responsibility actually gives you
Before we jump into code, it is worth stating what the pattern does (and what it does not).
Chain of Responsibility is a routing mechanism.
- It lets you split decision logic into small handlers
- It makes the order explicit
- It allows the chain to be configured dynamically (if needed)
- It keeps each handler focused on one responsibility
It does not magically make logic better. If handlers are poorly defined, the chain will still be messy. But the structure makes it much harder to accidentally create a “god function”.
A minimal model: user state
Let’s use a simple example: user state.
enum UserState: Equatable {
case onboarding
case needsPasswordChange
case blocked(reason: String)
case authorized
}The goal is to process a state and decide what to do next (navigate, show a modal, block access, etc.).
A handler protocol with a real result type
A common mistake in CoR examples is using Void return type. It works for demos, but it is weak design for production code because you cannot tell whether the request was processed.
enum UserStateAction: Equatable {
case showOnboarding
case showPasswordChange
case showBlocked
case proceed
}Now we can define a handler:
protocol UserStateHandler {
var next: UserStateHandler? { get }
func handle(_ state: UserState) -> UserStateAction?
}A handler either returns an action or forwards the state to the next handler.
Base handler implementation
We can reduce boilerplate by adding a base implementation:
extension UserStateHandler {
func forward(_ state: UserState) -> UserStateAction? {
next?.handle(state)
}
}This keeps every handler small.
Concrete handlers
Each handler has one responsibility.
Onboarding handler
struct OnboardingHandler: UserStateHandler {
let next: UserStateHandler?
func handle(_ state: UserState) -> UserStateAction? {
guard state == .onboarding else { return forward(state) }
return .showOnboarding
}
}Password change handler
struct PasswordChangeHandler: UserStateHandler {
let next: UserStateHandler?
func handle(_ state: UserState) -> UserStateAction? {
guard state == .needsPasswordChange else { return forward(state) }
return .showPasswordChange
}
}Blocked handler
struct BlockedHandler: UserStateHandler {
let next: UserStateHandler?
func handle(_ state: UserState) -> UserStateAction? {
guard state == .blocked else { return forward(state) }
return .showBlocked
}
}Default handler (terminal handler)
A chain without a default handler creates a “no result” scenario. Some teams are fine with that. Most apps should not be.
You can create a terminal handler that guarantees an action.
struct DefaultHandler: UserStateHandler {
let next: UserStateHandler? = nil
func handle(_ state: UserState) -> UserStateAction? {
.proceed
}
}Building the chain
The order is not is part of the business logic.
With Swift value types, chain building is also clean and explicit:
func makeUserStateChain() -> UserStateHandler {
let tail = DefaultHandler()
let blocked = BlockedHandler(next: tail)
let password = PasswordChangeHandler(next: blocked)
let onboarding = OnboardingHandler(next: password)
return onboarding
}This reads like a pipeline.
Using the chain
let chain = makeUserStateChain()
let state: UserState = .blocked
guard let action = chain.handle(state) else { return }
switch action {
case .showOnboarding:
print("Navigate to onboarding")
case .showPasswordChange:
print("Navigate to password change")
case .showBlocked(let reason):
print("Show blocked screen: \(reason)")
case .proceed:
print("Continue into the app")
}Each rule lives in one place.
Dynamic configuration
Sometimes you do not want the chain to be fixed. Example: features behind A/B test, remote config, or build flags.
The simplest approach is to build different chains.
func makeUserStateChain(isPasswordChangeEnabled: Bool) -> UserStateHandler {
let tail = DefaultHandler()
let blocked = BlockedHandler(next: tail)
if isPasswordChangeEnabled {
let password = PasswordChangeHandler(next: blocked)
return OnboardingHandler(next: password)
} else {
return OnboardingHandler(next: blocked)
}
}
This is still easy to read and safe.
Chain of responsibility vs state pattern
These patterns can look similar, especially when the input is an enum.
The difference is intent.
- State pattern changes object behavior depending on state and usually models transitions between states.
- Chain of Responsibility routes a request through handlers until one of them handles it.
In this article, we are not storing a “current state object” and switching behavior based on it. We are routing a single request through a pipeline of rules. That is a chain.
Common pitfalls
Handlers doing too much
A handler should not become a mini service that performs five checks and coordinates multiple flows. If it grows, it is usually a sign you need more handlers.
Silent failures
If no handler processes the request, your logic becomes implicit. Use a terminal handler if you expect the chain to always return an action.
Unclear ordering rules
If ordering matters, make it obvious in code. If ordering should not matter, you might not need CoR at all.
When I would use this pattern
In iOS projects, CoR fits nicely in areas like:
- deep link routing
- access control checks
- request validation pipelines
- feature gate checks
- UI flow selection based on conditions
The rule of thumb is simple: if you see a growing conditional block that decides “what to do next”, a chain is worth considering.
Closing thoughts
Chain of Responsibility is a small pattern with a big practical payoff. It does not require complex architecture. It just gives you a clean way to express a set of rules as a pipeline.
And more importantly, it makes adding the next rule boring. That is exactly what you want.
I hope this was interesting and not too boring for you. The following article will explore another design pattern called the Command.
Check out other posts in the Design Patterns series:
- Visitor Design Pattern in Swift
- Template Method Design Pattern in Swift
- Strategy Design Pattern in Swift
- State Design Pattern in Swift
- Observer Design Pattern in Swift
- Memento Design Pattern in Swift
- Mediator Design Pattern in Swift
- Iterator Design Pattern in Swift
- Command Design Pattern in Swift
- Proxy Design Pattern in Swift
- FlyWeight Design Pattern in Swift
- Facade Design Pattern in Swift
- Decorator Design Pattern in Swift
- Composite Design Pattern in Swift
- Bridge Design Pattern in Swift
- Adapter Design Pattern in Swift
- Singleton Design Pattern in Swift
- Prototype Design Pattern in Swift
- Builder Design Pattern in Swift
- Abstract Factory Design Pattern in Swift
- Factory Method Design Pattern in Swift
- Design Patterns: Basics
