Build reusable Clojure modules with plain data, small functions, higher-order workflows, and dispatch only where real polymorphism is needed.
Java developers often learn reuse through types: interfaces, abstract classes, decorators, strategies, service objects, and dependency injection containers. Those tools can work well, but they also create pressure to solve every variation with more structure.
Clojure takes a different path. Reuse usually starts with:
The result is modularity with less scaffolding. You spend less effort deciding where a behavior “belongs” in a class hierarchy and more effort deciding which transformations should be combined.
| Reuse problem | Common Java solution | Common Clojure solution |
|---|---|---|
| Swap one algorithm in a workflow | Strategy interface | Pass a function |
| Add optional behavior around a step | Decorator object | Compose another function |
| Share a domain contract | Interface plus DTO class | Documented/validated map shape |
| Extend behavior by type or category | Interface hierarchy | Protocol or multimethod when needed |
If behavior is a value, you can pass it into a general workflow instead of inventing a new type to host it.
1(defn import-users [parse-row valid? enrich rows]
2 (->> rows
3 (map parse-row)
4 (filter valid?)
5 (map enrich)))
Now the variability is explicit:
parse-row decides how a row becomes a mapvalid? decides which rows surviveenrich decides what additional fields to computeThe workflow stays the same while the behavior is swapped at the call site.
1(import-users csv->user user-valid? attach-default-flags rows)
2(import-users api-payload->user partner-user-valid? attach-source-tag payloads)
That is the functional version of a strategy pattern, but without extra ceremony.
If the call site starts to become noisy, bundle related behavior in a map:
1(def csv-import
2 {:parse csv->user
3 :valid? user-valid?
4 :enrich attach-default-flags})
5
6(defn import-with [strategy rows]
7 (->> rows
8 (map (:parse strategy))
9 (filter (:valid? strategy))
10 (map (:enrich strategy))))
This keeps the strategy visible as data. You can still validate it, test it, and pass it around without introducing a class hierarchy.
Clojure namespaces still matter. They are the unit of organization and API exposure. But within a namespace, reuse often looks like composing transformations:
1(ns my.app.pricing)
2
3(defn apply-discount [order]
4 (update order :total-cents - (:discount-cents order 0)))
5
6(defn apply-tax [order]
7 (update order :total-cents + (:tax-cents order 0)))
8
9(defn finalize-order [order]
10 (-> order
11 apply-discount
12 apply-tax
13 (assoc :status :ready)))
Each helper is independently testable. finalize-order is reusable because it describes the order of the transformations clearly, not because it inherits behavior from a base type.
A namespace can become a dumping ground if you treat it like a static utility class. A better rule is to expose a few stable functions that describe the module’s real jobs, then keep the smaller helpers private or local when they are only implementation detail.
1(ns my.app.pricing)
2
3(defn price-order [catalog order]
4 (-> order
5 (attach-line-prices catalog)
6 apply-discount
7 apply-tax
8 finalize-total))
The caller should not need to know every helper in the pricing pipeline. That gives you the same encapsulation goal Java developers value, but the boundary is a namespace API rather than a class with private fields.
In Java, interfaces are often used to create a common contract among several implementations. In Clojure, a simple map shape is often enough:
1{:id 42
2 :plan :team
3 :status :trial
4 :discount-cents 500}
If multiple functions agree on that data shape, you already have a useful contract. You may still document it, test it, or validate it with clojure.spec or Malli later. The key point is that the contract is expressed through data and behavior, not only through a type declaration.
Functional design does not mean “never use dispatch.” It means “use dispatch when the problem truly requires it.”
Reach for protocols or multimethods when:
case or cond branches is becoming noisyDo not reach for them just because a Java design would start with an interface. Many workflows are clearer when they stay as ordinary functions over maps.
| If the variation is… | Prefer… | Reason |
|---|---|---|
| One callback in one workflow | Function argument | Smallest useful abstraction |
| A named bundle of callbacks | Map of functions | Keeps configuration data-oriented |
| Closed set of simple cases | case or cond |
Easy to read and test |
| Open extension by type/protocol | Protocol | Useful when callers add implementations |
| Open extension by data attributes | Multimethod | Dispatch can be based on more than Java type |
For Java engineers moving into Clojure, the most valuable question changes from:
“Which class should own this behavior?”
to:
“Which transformations are stable enough to name and compose?”
That shift matters because composition is cheaper than inheritance-heavy reuse:
The common mistake is replacing class-heavy design with anonymous-function-heavy design that nobody can read. Reusable Clojure code still needs:
If a composed workflow is hard to explain, it is not reusable yet. Name the pieces first. Then compose them.