Browse Clojure Foundations for Java Developers

Modularity and Reusability

Reuse behavior by composing small functions and data transformations instead of coupling logic to class hierarchies.

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:

  • plain data as the input and output shape
  • small functions that do one thing
  • composition of those functions into a larger workflow

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 By Passing Behavior

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 map
  • valid? decides which rows survive
  • enrich decides what additional fields to compute

The 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.

Composition Is Often The Primary Module Boundary

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.

Data Shapes Replace A Lot Of Interface Noise

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.

Protocols And Multimethods Are For Real Polymorphism

Functional design does not mean “never use dispatch.” It means “use dispatch when the problem truly requires it.”

Reach for protocols or multimethods when:

  • behavior genuinely varies by category of data
  • you need open extension points
  • a pile of case or cond branches is becoming noisy

Do 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.

What Reuse Looks Like In Practice

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:

  • less wiring
  • fewer fake seams
  • easier testing
  • easier recombination of behavior in new workflows

The Main Failure Mode

The common mistake is replacing class-heavy design with anonymous-function-heavy design that nobody can read. Reusable Clojure code still needs:

  • named functions for important concepts
  • coherent namespaces
  • clear data shapes
  • restraint around clever abstraction

If a composed workflow is hard to explain, it is not reusable yet. Name the pieces first. Then compose them.

Knowledge Check: Modularity

### In functional design, what often replaces “strategy objects” for swapping behavior? - [x] Passing a function (or a map of functions) as an argument. - [ ] Adding another abstract base class. - [ ] Relying on global mutable state. - [ ] Using reflection at runtime. > **Explanation:** If behavior is a value, you can pass it directly into a workflow instead of creating a class just to swap one algorithm. ### Why does composition tend to improve reuse? - [x] Small functions can be combined in different orders and contexts without tight coupling. - [ ] Composed code can’t be tested. - [ ] Composition forces you to avoid data structures. - [ ] Composition only works for numeric algorithms. > **Explanation:** Small functions with clear inputs and outputs are easier to mix, test, and reuse than behavior tied to a rigid object hierarchy. ### When do protocols/multimethods become relevant? - [x] When you need open, extensible polymorphism across different kinds of data. - [ ] Whenever you want to avoid passing functions. - [ ] Whenever you want to mutate state. - [ ] Whenever you want to replace `map`. > **Explanation:** Protocols and multimethods are useful when data plus ordinary functions no longer express the needed variation cleanly. They are a tool for real dispatch, not the starting point for every module.
Revised on Friday, April 24, 2026