Apply practical Clojure patterns such as pipelines, higher-order functions, reducers, dependency maps, middleware, and data interpreters when migrating Java behavior.
Functional design patterns in Clojure are not a separate catalog to memorize. They are recurring ways to keep behavior small, data explicit, and side effects visible. For Java developers, the useful patterns are the ones that make migration safer: pipelines, higher-order functions, reducers, dependency maps, middleware, and interpreters over data.
These patterns are practical because they reduce the need for object scaffolding. They also make tests easier to write before and after migration.
A pipeline expresses a sequence of transformations over a value.
1(defn normalize-order [order]
2 (-> order
3 (update :order/email clojure.string/lower-case)
4 (update :order/items #(remove :item/cancelled? %))
5 (assoc :order/status :ready-for-pricing)))
Use -> when the value flows through the first argument position. Use ->> when a collection flows through the last argument position.
1(defn billable-items [items]
2 (->> items
3 (filter :item/billable?)
4 (map #(select-keys % [:item/sku :item/quantity :item/price]))))
Pipelines are best when each step has a small, named responsibility. If the pipeline becomes hard to review, extract named functions.
Java loops with mutable accumulators often become reduce.
1(defn totals-by-sku [items]
2 (reduce (fn [totals {:item/keys [sku quantity price]}]
3 (update totals sku (fnil + 0M) (* quantity price)))
4 {}
5 items))
Use a reducer when many inputs become one result: a map, total, grouped value, summary, validation report, or decision. Keep the reducing function pure unless the whole reducer is explicitly at an effectful boundary.
Higher-order functions let you pass behavior without building classes.
1(defn retrying [attempts operation]
2 (fn [& args]
3 (loop [remaining attempts]
4 (let [result (apply operation args)]
5 (if (or (:ok? result) (zero? remaining))
6 result
7 (recur (dec remaining)))))))
This can replace small strategy, command, retry, validation, or policy objects. Use it when the behavior is genuinely parameterized and the resulting function remains easy to name and test.
Dependency maps are a lightweight alternative to a dependency injection container for migrated slices.
1(defn submit-invoice! [{:keys [save! publish! now]} invoice]
2 (let [saved (save! (assoc invoice :invoice/submitted-at (now)))]
3 (publish! :invoice.submitted {:invoice/id (:invoice/id saved)})
4 saved))
In tests, dependencies can be plain functions.
1(def test-deps
2 {:save! identity
3 :publish! (fn [_topic _payload] nil)
4 :now (constantly #inst "2026-01-01T00:00:00.000-00:00")})
This pattern keeps side effects explicit without requiring a framework. For larger systems, you may still use a component framework, but the function boundary remains the same.
Middleware wraps a handler with cross-cutting behavior.
1(defn with-correlation-id [handler]
2 (fn [request]
3 (let [request-id (or (:request/id request) (str (random-uuid)))]
4 (handler (assoc request :request/id request-id)))))
5
6(defn with-exception-data [handler]
7 (fn [request]
8 (try
9 (handler request)
10 (catch Exception ex
11 {:status 500
12 :error/message (.getMessage ex)
13 :request/id (:request/id request)}))))
Middleware is useful for request handlers, message handlers, validation shells, and Java adapter boundaries. Keep wrapper order visible and tested.
Sometimes the best functional pattern is data first, execution second. Commands, workflows, and validation results can be represented as values, then interpreted at the boundary.
1(defn invoice-commands [invoice]
2 (cond-> []
3 (:invoice/valid? invoice)
4 (conj {:command/type :save-invoice
5 :invoice invoice})
6
7 (:invoice/valid? invoice)
8 (conj {:command/type :publish-event
9 :event/type :invoice.submitted
10 :invoice/id (:invoice/id invoice)})))
11
12(defn run-command! [{:keys [save! publish!]} command]
13 (case (:command/type command)
14 :save-invoice (save! (:invoice command))
15 :publish-event (publish! (:event/type command) command)))
This pattern is valuable in migration because pure code can return commands without performing writes. Tests inspect the commands; the shell performs them.
| Need | Clojure pattern |
|---|---|
| Transform a value through stages | Pipeline |
| Collapse many inputs into one result | Reducer |
| Swap a behavior | Higher-order function |
| Pass side-effecting collaborators | Dependency map |
| Add cross-cutting behavior | Middleware |
| Separate decision from execution | Data interpreter |
Use these as engineering tools, not decorative pattern labels. The code should become easier to test, not just easier to name.