Chain of Responsibility with Composed Functions

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?”

A Functional Version of the Pattern

Each handler can be a function that either:

  • returns a handled result
  • returns the request for further processing
  • returns 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.

Pipelines Work When the Contract Is Clear

This pattern works well in Clojure when each handler has a clean contract:

  • same input shape
  • same output shape for a handled result
  • explicit “pass” behavior

Without that consistency, the chain becomes hard to reason about because every handler starts inventing its own rules for what counts as “handled.”

A Useful Variation: Transform Then Decide

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.

The Flow at a Glance

    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.

Where It Fits Well

This pattern is strong for:

  • authorization or approval pipelines
  • request validation layers
  • parsing fallbacks
  • content negotiation
  • middleware-like processing

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.

Common Failure Modes

Chain of Responsibility gets messy when:

  • handlers mutate hidden shared state
  • pass vs handled rules are inconsistent
  • too many unrelated concerns are shoved into one chain
  • the order is accidental rather than deliberate

If changing the handler order silently changes semantics, the order should probably be documented or tested explicitly.

Practical Heuristics

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.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026