Pass it along until someone handles it
Calling a company’s support line works like this. The first person you reach answers simple questions. If yours is about billing, they pass you to billing. If billing can’t help, they pass you to a specialist. You only ever dialed one number; you don’t need to know who’s behind it or in what order.
In code, each person is a handler with one question to answer: “can I handle this?” If yes, it does the work and the request stops there. If no, it passes the request to the next handler. The code that starts the request only knows the first handler.
Chain of Responsibility decouples the sender of a request from its receivers by giving several objects a chance to handle it, passing it along a chain until one does. In its classic form exactly one handler handles the request and the rest never see it. Its strengths: handlers are independent, testable in isolation, and the chain is assembled (and reordered) at the composition root. Its costs: no compile-time guarantee that anything handles the request (you need a terminal fallback), and behavior depends on order, which is now configuration. A list of handlers with “first that returns a result wins” is the same pattern without linked next pointers, and is usually simpler.
enum Route { case order(String), product(String), sale, promo(String), home }
protocol DeepLinkHandler {
func route(for url: URL) -> Route? // nil means "not mine, pass it on"
}
struct OrderLinkHandler: DeepLinkHandler {
func route(for url: URL) -> Route? {
let parts = url.pathComponents // ["/", "orders", "123"]
guard parts.count == 3, parts[1] == "orders" else { return nil }
return .order(parts[2])
}
}
struct SaleLinkHandler: DeepLinkHandler {
func route(for url: URL) -> Route? { url.path == "/products/sale" ? .sale : nil }
}
struct ProductLinkHandler: DeepLinkHandler {
func route(for url: URL) -> Route? {
let parts = url.pathComponents
guard parts.count == 3, parts[1] == "products" else { return nil }
return .product(parts[2])
}
}
struct DeepLinkRouter {
let handlers: [DeepLinkHandler]
func route(for url: URL) -> Route {
handlers.lazy.compactMap { $0.route(for: url) }.first ?? .home // first match wins
}
}
let router = DeepLinkRouter(handlers: [OrderLinkHandler(), SaleLinkHandler(), ProductLinkHandler(), PromoLinkHandler()])sealed interface Route {
data class Order(val id: String) : Route
data class Product(val id: String) : Route
data object Sale : Route
data class Promo(val code: String) : Route
data object Home : Route
}
fun interface DeepLinkHandler {
fun route(uri: Uri): Route? // null means "not mine, pass it on"
}
val orderLinks = DeepLinkHandler { uri ->
val s = uri.pathSegments // ["orders", "123"]
if (s.size == 2 && s[0] == "orders") Route.Order(s[1]) else null
}
val saleLinks = DeepLinkHandler { uri -> if (uri.path == "/products/sale") Route.Sale else null }
val productLinks = DeepLinkHandler { uri ->
val s = uri.pathSegments
if (s.size == 2 && s[0] == "products") Route.Product(s[1]) else null
}
class DeepLinkRouter(private val handlers: List<DeepLinkHandler>) {
fun route(uri: Uri): Route =
handlers.firstNotNullOfOrNull { it.route(uri) } ?: Route.Home // first match wins
}
val router = DeepLinkRouter(listOf(orderLinks, saleLinks, productLinks, promoLinks))public sealed interface Route {
record Order(String id) implements Route {}
record Product(String id) implements Route {}
record Sale() implements Route {}
record Promo(String code) implements Route {}
record Home() implements Route {}
}
@FunctionalInterface
public interface DeepLinkHandler {
Optional<Route> route(URI uri); // empty means "not mine, pass it on"
}
DeepLinkHandler orderLinks = uri -> {
String[] s = uri.getPath().split("/"); // ["", "orders", "123"]
return s.length == 3 && s[1].equals("orders") ? Optional.of(new Route.Order(s[2])) : Optional.empty();
};
DeepLinkHandler saleLinks = uri ->
uri.getPath().equals("/products/sale") ? Optional.of(new Route.Sale()) : Optional.empty();
DeepLinkHandler productLinks = uri -> {
String[] s = uri.getPath().split("/");
return s.length == 3 && s[1].equals("products") ? Optional.of(new Route.Product(s[2])) : Optional.empty();
};
public record DeepLinkRouter(List<DeepLinkHandler> handlers) {
public Route route(URI uri) {
return handlers.stream()
.map(h -> h.route(uri))
.flatMap(Optional::stream)
.findFirst() // first match wins (streams are lazy)
.orElse(new Route.Home());
}
}type Route =
| { kind: "order"; id: string }
| { kind: "product"; id: string }
| { kind: "sale" }
| { kind: "promo"; code: string }
| { kind: "home" };
type DeepLinkHandler = (url: URL) => Route | null; // null means "not mine, pass it on"
const orderLinks: DeepLinkHandler = (url) => {
const s = url.pathname.split("/"); // ["", "orders", "123"]
return s.length === 3 && s[1] === "orders" ? { kind: "order", id: s[2] } : null;
};
const saleLinks: DeepLinkHandler = (url) =>
url.pathname === "/products/sale" ? { kind: "sale" } : null;
const productLinks: DeepLinkHandler = (url) => {
const s = url.pathname.split("/");
return s.length === 3 && s[1] === "products" ? { kind: "product", id: s[2] } : null;
};
function createRouter(handlers: DeepLinkHandler[]) {
return (url: URL): Route => {
for (const handle of handlers) {
const route = handle(url);
if (route) return route; // first match wins
}
return { kind: "home" };
};
}
const route = createRouter([orderLinks, saleLinks, productLinks, promoLinks]);- Each handler knows one kind of link and returns a route, or “nothing” to say “not mine”.
- The router asks the handlers in order and stops at the first one that answers. If none does, the app goes to the home screen, so an unknown link never crashes it.
- Adding a screen means writing one handler and adding it to the list. Nobody else’s code changes, and each handler can be tested with a few URLs.
- Order matters:
/products/salealso looks like a product link with idsale. The specific sale handler must come before the general product handler.
Tap the line with the bug.
let router = DeepLinkRouter(handlers: [
OrderLinkHandler(),
ProductLinkHandler(),
SaleLinkHandler(),
PromoLinkHandler(),
])val router = DeepLinkRouter(
listOf(
orderLinks,
productLinks,
saleLinks,
promoLinks,
)
)var router = new DeepLinkRouter(List.of(
orderLinks,
productLinks,
saleLinks,
promoLinks
));const route = createRouter([
orderLinks,
productLinks,
saleLinks,
promoLinks,
]);Hint: Follow /products/sale through the list. Which handler answers first?
The general product handler comes before the sale handler. For /products/sale it sees two segments starting with products, happily returns “product with id sale”, and the chain stops there. The sale handler never runs, and the app opens a product page that fails to load product “sale”.
In a first-match chain, specific handlers go before general ones. Swap them, and add a test that routes every known link, so the order is checked by CI instead of by users:
let router = DeepLinkRouter(handlers: [
OrderLinkHandler(),
SaleLinkHandler(), // specific before general
ProductLinkHandler(),
PromoLinkHandler(),
])val router = DeepLinkRouter(listOf(orderLinks, saleLinks, productLinks, promoLinks))var router = new DeepLinkRouter(List.of(orderLinks, saleLinks, productLinks, promoLinks));const route = createRouter([orderLinks, saleLinks, productLinks, promoLinks]);Predict the output
With the fixed order (orders, sale, products, promo), what does the router return for /support?
Every handler returns 'not mine', the chain runs out, and the router's fallback returns home. Without a fallback, a chain can simply end with nobody handling the request, which is why the end of the chain must decide what 'nobody handled it' means: a default screen, a 404, an error log.
- Sender knows only the chain; handlers decide to handle or pass. Classic form: exactly one handler handles.
- Order is configuration: specific before general; always a terminal fallback (home, 404, error).
- A list of handlers with “first non-null wins” is the same pattern without
nextpointers, and usually simpler. - Test the assembled chain against every known input, not just each handler alone.
Check this part
What does a handler do with a request it can't handle?
Why must the sale handler come before the product handler?
What guarantees that every request is handled?