Use higher-order functions and composition to assemble behavior from small pure steps in idiomatic Clojure.
Higher-order functions are functions that take other functions as arguments, return functions, or both. In Clojure, that is not an advanced corner of the language. It is one of the main ways programs stay small, composable, and explicit.
That matters because many “design patterns” in Clojure are really just disciplined use of higher-order functions. Strategy, middleware, validation pipelines, dependency injection, and event processing often reduce to passing behavior around as data-like values rather than building class hierarchies.
Since functions are ordinary values, you can:
This gives you a different kind of reuse than object-oriented inheritance. Instead of sharing behavior through type hierarchies, you assemble behavior from smaller parts.
Suppose you are normalizing and validating user input:
1(ns app.user)
2
3(defn trim-email [s]
4 (clojure.string/trim s))
5
6(defn lower-email [s]
7 (clojure.string/lower-case s))
8
9(defn ensure-at-sign [s]
10 (when-not (clojure.string/includes? s "@")
11 (throw (ex-info "Invalid email" {:type ::invalid-email})))
12 s)
13
14(def normalize-email
15 (comp ensure-at-sign lower-email trim-email))
normalize-email is not a class or a service object. It is a composed function that expresses a stable processing pipeline. The order is readable and the pieces are independently testable.
Composition is useful when a transformation has a few properties:
The diagram below shows the shape:
flowchart LR
Raw["Raw input"] --> Trim["Trim"]
Trim --> Normalize["Normalize"]
Normalize --> Validate["Validate"]
Validate --> Final["Final value"]
This style is all over idiomatic Clojure:
comp, partial, And ClosuresThree tools show up repeatedly.
comp builds a new function from smaller ones. It is best when the data shape flows cleanly from one step to the next.
partial fixes some arguments in advance. That is useful when you want a specialized function without creating an entire wrapper by hand.
Closures capture surrounding values. That lets you build configured behavior on demand.
1(defn min-length [n]
2 (fn [s]
3 (<= n (count s))))
4
5(def at-least-8? (min-length 8))
This is still just function construction. No extra framework is needed.
The first mistake is composing functions that do not really belong together. A pipeline should express a coherent flow, not become a random bag of transformations.
The second mistake is hiding side effects inside steps that look pure. If one function in the middle sends metrics or hits a database, the composition becomes harder to reason about.
The third mistake is over-composition. Sometimes one small direct function is clearer than five tiny helpers assembled with comp.
In Clojure, higher-order functions often replace heavier pattern machinery:
This is one reason Clojure pattern catalogs need different explanations than Java ones. The language changes the pattern shape.
When reviewing higher-order function design, ask:
Good higher-order code feels smaller after composition, not more abstract.