Move from mutable Java loops and object updates to Clojure functions over immutable data, with clear examples of reduce, filter, value updates, and explicit side-effect boundaries.
The shift from imperative Java to functional Clojure is mostly a shift in how you model change. In Java, you often change a local variable, mutate an object, append to a collection, or call a method that hides a state transition. In Clojure, you usually derive a new value from an old value and pass that value forward.
Functional core: The part of a program that computes results from inputs without observable side effects. In Clojure, you usually try to make this core large and keep I/O at the edges.
| Question | Imperative Java answer | Functional Clojure answer |
|---|---|---|
| How do I accumulate a result? | Mutate a variable in a loop | Use reduce or a pipeline |
| How do I update data? | Call a setter or mutate a collection | Return a new value with assoc, update, or conj |
| How do I share logic? | Put methods on classes | Compose functions over data |
| How do I control effects? | Rely on object boundaries and discipline | Keep effects in visible boundary functions |
| How do I test behavior? | Build object fixtures and mocks | Call pure functions with plain data |
This is not about making Java “wrong.” It is about choosing defaults that make Clojure easier to reason about.
Java loop with a mutable accumulator:
1int sum = 0;
2for (int number : numbers) {
3 sum += number;
4}
Clojure reduction:
1(reduce + numbers)
The Clojure version says what the calculation is: combine the values with +. There is no mutable sum slot to track.
For a more realistic shape:
1(def orders
2 [{:order/id 1 :order/total-cents 1200 :order/status :paid}
3 {:order/id 2 :order/total-cents 800 :order/status :open}
4 {:order/id 3 :order/total-cents 500 :order/status :paid}])
5
6(defn paid-total-cents [orders]
7 (->> orders
8 (filter #(= :paid (:order/status %)))
9 (map :order/total-cents)
10 (reduce + 0)))
The pipeline reads as data flow: keep paid orders, extract totals, add them.
Java update:
1order.setStatus(OrderStatus.PAID);
2order.setPaidAt(clock.instant());
Clojure update:
1(defn mark-paid [order paid-at]
2 (assoc order
3 :order/status :paid
4 :order/paid-at paid-at))
mark-paid returns a new order value. The caller decides what to do with it. That makes review easier because the state transition is explicit in the return value.
Java often introduces an interface when behavior must vary:
1interface DiscountPolicy {
2 int discount(Order order);
3}
In Clojure, functions are already values:
1(defn apply-discount [discount-fn order]
2 (update order :order/total-cents - (discount-fn order)))
3
4(defn vip-discount [order]
5 (if (:order/vip? order) 500 0))
This is not “less design.” It is less scaffolding around the same design force: pass behavior where behavior must vary.
Functional Clojure still does I/O, logging, database writes, and Java interop. The difference is where those effects live.
1(defn receipt [order]
2 {:receipt/order-id (:order/id order)
3 :receipt/total-cents (:order/total-cents order)})
4
5(defn email-receipt! [send-email! order]
6 (send-email! (:order/customer-email order)
7 (receipt order)))
receipt is pure and easy to test. email-receipt! is the boundary that performs an effect. The ! in the name is a convention that warns readers about the side effect.
flowchart LR
A["Mutable Java loop"] --> B["Hidden changing state"]
B --> C["More setup for tests"]
D["Clojure data pipeline"] --> E["Explicit values"]
E --> F["Direct input/output tests"]
The shift is not that every program becomes a pipeline. The shift is that values and transformations become the default design vocabulary.
filter plus into [] pipeline.! to one side-effecting function name and explain why the effect belongs at that boundary.reduce, filter, map, and update replace many loops and setters.