Browse Learn Clojure Foundations as a Java Developer

Use Functional Design Patterns in Clojure

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.

Pipeline Pattern

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.

Reducer Pattern

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 Function Pattern

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 Map Pattern

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 Pattern

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.

Data Interpreter Pattern

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.

Pattern Selection Guide

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.

Practice

  1. Rewrite a Java accumulator loop as a reducer.
  2. Replace a small strategy object with a function argument.
  3. Extract one effectful dependency into a dependency map.
  4. Represent one command as data and interpret it at the boundary.

Key Takeaways

  • Functional patterns help keep behavior small, data explicit, and effects visible.
  • Pipelines and reducers replace much of Java’s loop-and-builder code.
  • Dependency maps and middleware make migrated boundaries testable.
  • Data interpreters are useful when you need decisions to be inspectable before effects run.
  • Prefer patterns that reduce migration risk and improve reviewability.

Quiz: Functional Design Patterns

### What is a good use for a pipeline? - [x] Transforming a value through a clear sequence of steps. - [ ] Storing global mutable state. - [ ] Replacing every Java interface. - [ ] Hiding side effects. > **Explanation:** Pipelines make staged transformations visible. ### When is `reduce` usually appropriate? - [x] When many inputs become one accumulated result. - [ ] When you need framework lifecycle callbacks. - [ ] When you want to mutate a local Java variable. - [ ] When every function performs I/O. > **Explanation:** Reducers replace accumulator loops with pure accumulation. ### Why use a dependency map? - [x] To pass effectful collaborators explicitly and make tests direct. - [ ] To recreate a dependency injection container in every function. - [ ] To avoid naming dependencies. - [ ] To make global state implicit. > **Explanation:** Dependency maps keep boundaries visible without framework overhead. ### What does middleware do? - [x] Wraps a handler with additional behavior. - [ ] Converts maps into Java classes. - [ ] Forces inheritance. - [ ] Removes tests. > **Explanation:** Middleware composes cross-cutting behavior around a handler. ### Why represent commands as data? - [x] Pure code can return planned effects for tests to inspect before a boundary executes them. - [ ] Data commands execute themselves automatically. - [ ] Java cannot call Clojure functions. - [ ] It prevents all errors. > **Explanation:** Data interpreters separate decision from execution.
Revised on Saturday, May 23, 2026