Function Composition and Pipelines in Clojure

Build readable data-processing flows with `comp`, threading macros, and clear transformation boundaries in Clojure.

Pipeline composition: A way of expressing a larger transformation as a sequence of smaller steps, each with a clear input and output shape.

Clojure programs are often easiest to understand when data moves through a visible pipeline. Instead of burying logic inside large functions or mutable objects, you keep each step small, name the transformations clearly, and decide whether you are composing functions or threading a concrete value through operations.

That distinction matters. Clojure gives you both comp and threading macros because they solve related but different problems.

Choose The Right Composition Tool

Use comp when you want to define a reusable function ahead of time.

1(def normalize-email
2  (comp clojure.string/lower-case
3        clojure.string/trim))

Use -> when you are walking a single value through operations that conceptually read left to right.

1(-> user
2    :email
3    clojure.string/trim
4    clojure.string/lower-case)

Use ->> when the collection or stream should land in the last argument position.

1(->> orders
2     (filter :paid?)
3     (map :total)
4     (reduce + 0))

Use as-> when the shape changes and neither -> nor ->> stays readable.

1(as-> raw-data $
2  (json/parse-string $ true)
3  (:payload $)
4  (select-keys $ [:id :status :items]))

Make The Data Shape Obvious

Pipelines stay maintainable when each step has a predictable contract. A reviewer should be able to answer these questions quickly:

  • what shape comes in here
  • what shape leaves here
  • where does validation happen
  • where do effects begin

That means the best pipelines are usually shaped around ordinary maps, vectors, sequences, and explicit result values, not hidden mutable state.

 1(defn parse-order [payload]
 2  (json/parse-string payload true))
 3
 4(defn select-order-fields [order]
 5  (select-keys order [:id :customer-id :items :currency]))
 6
 7(defn attach-item-count [order]
 8  (assoc order :item-count (count (:items order))))
 9
10(def process-order
11  (comp attach-item-count
12        select-order-fields
13        parse-order))

Each step is understandable on its own. That is more important than whether the code looks “functional enough.”

Separate Pure Steps From Effect Boundaries

A healthy pipeline usually has a pure middle and a small imperative edge.

 1(defn enrich-order [order]
 2  (-> order
 3      normalize-order
 4      attach-item-count
 5      attach-risk-flags))
 6
 7(defn save-order! [db order]
 8  (jdbc/execute! db ["insert into orders ..." order]))
 9
10(defn handle-order! [db payload]
11  (-> payload
12      parse-order
13      enrich-order
14      (save-order! db)))

save-order! is not a hidden step in the middle of a “pure” pipeline. It is the explicit effect boundary. That makes testing and reasoning simpler.

Composition Is Not The Same As Good Design

You can overdo this pattern. A pile of one-line helpers with vague names is not automatically clearer than one well-structured function. The point of composition is to reveal the flow, not to multiply indirection.

Common failure modes:

  • composing steps that do unrelated jobs
  • mixing validation, enrichment, persistence, and logging without clear boundaries
  • hiding side effects inside apparently pure helpers
  • forcing comp where -> would be easier to read

Pipelines In Larger Systems

This pattern appears all over idiomatic Clojure:

  • Ring middleware chains
  • ETL transformations
  • event enrichment stages
  • parser and normalization layers
  • transducer-based processing

The underlying idea is the same: keep each step honest, predictable, and small enough to replace without rewriting the whole flow.

Design Review Questions

When reviewing pipeline-heavy Clojure code, ask:

  • Is the chosen tool appropriate: comp, ->, ->>, or as->?
  • Do step names describe a real transformation?
  • Are effects visible at the boundary instead of hidden in the middle?
  • Would one direct function be simpler than a heavily fragmented chain?

Good pipelines make the data journey obvious. Bad ones turn simple work into ceremonial syntax.

Loading quiz…
Revised on Thursday, April 23, 2026