Use a repeatable recipe to move from mutable Java-style workflows to pure Clojure functions over immutable data.
The biggest question Java engineers usually have at this point is practical:
The answer is not “rewrite everything at once.”
The answer is to refactor in layers:
That gives you a repeatable migration path instead of a vague style aspiration.
Consider a Java-style pricing flow:
1BigDecimal subtotal = BigDecimal.ZERO;
2
3for (OrderItem item : items) {
4 if (item.isBillable()) {
5 subtotal = subtotal.add(
6 item.getUnitPrice().multiply(BigDecimal.valueOf(item.getQty()))
7 );
8 }
9}
10
11order.setSubtotal(subtotal);
12
13if (subtotal.compareTo(new BigDecimal("1000")) > 0) {
14 order.setStatus(OrderStatus.NEEDS_REVIEW);
15} else {
16 order.setStatus(OrderStatus.READY_TO_CHARGE);
17}
This works, but it mixes several concerns:
That mixture is exactly what makes later changes harder.
In Clojure, start by representing the order as plain data:
1(def order
2 {:order/id 1001
3 :items [{:sku "A-1" :qty 2 :unit-price 15M :billable? true}
4 {:sku "B-2" :qty 1 :unit-price 9M :billable? false}]})
This alone is a big shift. You stop centering the design on an object whose fields will be mutated and instead center it on a value you can transform.
Turn the subtotal logic into functions:
1(defn line-total [{:keys [qty unit-price]}]
2 (* qty unit-price))
3
4(defn subtotal [order]
5 (->> (:items order)
6 (filter :billable?)
7 (map line-total)
8 (reduce + 0M)))
Now the core pricing rule is explicit and testable on its own.
1(defn priced-order [order]
2 (assoc order :subtotal (subtotal order)))
Instead of “update the existing order object,” the question becomes:
That is the heart of the refactoring.
1(defn next-status [order]
2 (if (> (:subtotal order) 1000M)
3 :needs-review
4 :ready-to-charge))
5
6(defn advance-order [order]
7 (let [priced (priced-order order)]
8 (assoc priced :status (next-status priced))))
Now the whole business rule is pure.
That means:
If a real application needs to save or publish the result, keep that at the edge:
1(defn process-order! [repo order]
2 (let [next-order (advance-order order)]
3 (repo/save! repo next-order)
4 next-order))
This function is impure because it talks to persistence, but the important logic is no longer trapped inside the impure layer.
That is the design win you are aiming for.
When you see imperative code, ask:
Those questions often reveal the path from:
to:
A common beginner move is to replace Java object mutation with an atom too early.
That skips the most important step.
First refactor the business rule into pure functions over plain values. Only then decide whether the runtime needs:
If you reach for a state primitive before extracting the pure core, you often just rebuild the old imperative design with new syntax.
After refactoring, you should be able to evaluate the important business behavior at the REPL with plain data:
1(advance-order order)
If you still need:
then the pure core is probably not separated enough yet.