Learn how to model Chain of Responsibility in Clojure with composed handlers, early exit rules, and clear request contracts instead of class-based handler hierarchies.
Chain of Responsibility: A pattern where a request moves through a sequence of handlers until one handles it or the chain ends.
In Clojure, Chain of Responsibility is usually simpler as a pipeline of functions than as a linked hierarchy of handler objects. The main question is not “which class handles this?” but “how should requests move through a series of decision points?”
Each handler can be a function that either:
nil to indicate “not handled here”Here is one straightforward style:
1(defn try-admin [request]
2 (when (contains? (:roles request) :admin)
3 {:decision :allow :reason :admin}))
4
5(defn try-vip [request]
6 (when (:vip? request)
7 {:decision :allow :reason :vip}))
8
9(defn try-default [_]
10 {:decision :review :reason :standard-path})
11
12(defn run-chain [handlers request]
13 (some #(% request) handlers))
some makes the early-exit behavior explicit: the first truthy handled result wins.
This pattern works well in Clojure when each handler has a clean contract:
Without that consistency, the chain becomes hard to reason about because every handler starts inventing its own rules for what counts as “handled.”
Some chains are not just about yes/no handling. They enrich a request before the final decision:
1(defn attach-user [request]
2 (assoc request :user {:id 42 :roles #{:member}}))
3
4(defn attach-risk [request]
5 (assoc request :risk-score 18))
6
7(defn final-decision [{:keys [risk-score] :as request}]
8 (assoc request :decision (if (< risk-score 20) :allow :review)))
9
10(-> request
11 attach-user
12 attach-risk
13 final-decision)
That is structurally related to Chain of Responsibility, but the handlers are cooperating transformations rather than competing responders. Clojure often makes this distinction clearer than older pattern catalogs do.
flowchart LR
A["Request"] --> B["Handler 1"]
B -->|pass| C["Handler 2"]
B -->|handled| F["Result"]
C -->|pass| D["Handler 3"]
C -->|handled| F
D -->|handled or default| F
The point is early exit with a stable contract, not object inheritance.
This pattern is strong for:
It is weaker when all handlers need to collaborate on one shared outcome. In that case, a transformation pipeline or reduction may be more honest than a responsibility chain.
Chain of Responsibility gets messy when:
If changing the handler order silently changes semantics, the order should probably be documented or tested explicitly.
Use this pattern when handlers really are alternatives or staged gates. Prefer plain composed functions and clear return contracts. If every step must always run, you may not need a responsibility chain at all.