Browse Clojure Foundations for Java Developers

Refactoring Imperative Code

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:

  • “How do I actually move existing imperative code toward idiomatic Clojure?”

The answer is not “rewrite everything at once.”

The answer is to refactor in layers:

  1. identify the data
  2. isolate the pure calculation
  3. return new values instead of mutating old ones
  4. push I/O and framework calls outward

That gives you a repeatable migration path instead of a vague style aspiration.

Start With A Familiar Imperative Shape

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:

  • item selection
  • line-total calculation
  • subtotal accumulation
  • state mutation on the order
  • workflow decision logic

That mixture is exactly what makes later changes harder.

Step 1: Model The Data Directly

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.

Step 2: Extract The Pure Calculation

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.

Step 3: Derive The Next Value Instead Of Mutating The Old One

1(defn priced-order [order]
2  (assoc order :subtotal (subtotal order)))

Instead of “update the existing order object,” the question becomes:

  • what is the next order value?

That is the heart of the refactoring.

Step 4: Extract The Decision Logic Too

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:

  • no hidden object mutation
  • no required initialization order beyond explicit data
  • no framework dependence inside the pricing rule itself

Step 5: Leave Only The Effectful Shell Outside

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.

The Refactoring Recipe In Short

When you see imperative code, ask:

  1. Which parts are pure calculation?
  2. Which variables are acting like accumulators?
  3. Which object fields are being mutated only to carry a derived value?
  4. Which framework or I/O concerns can move to a thin shell?

Those questions often reveal the path from:

  • mutable workflow code

to:

  • immutable data plus pure functions

Do Not Refactor Straight Into State Primitives

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:

  • an atom
  • a ref
  • a database write
  • a message publish

If you reach for a state primitive before extracting the pure core, you often just rebuild the old imperative design with new syntax.

A Good Smell Test

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:

  • a live container
  • a database
  • mock collaborators
  • a pile of runtime setup

then the pure core is probably not separated enough yet.

Knowledge Check

### What is usually the first real step in refactoring imperative Java code toward idiomatic Clojure? - [x] Identify the data and extract the pure calculation before worrying about runtime state machinery - [ ] Replace every method with an atom - [ ] Convert all functions into macros - [ ] Add more setters for clarity > **Explanation:** The key early move is to find the pure transformation logic and make it explicit over plain data. ### Why is `priced-order` a better shape than mutating an order object in place? - [x] It makes the transformation explicit by returning the next value instead of hiding change in shared object state - [ ] It prevents all allocations - [ ] It eliminates the need for persistence - [ ] It requires no tests > **Explanation:** Returning a new value makes the data flow more visible and the logic easier to test and reason about. ### What should usually remain in the effectful shell? - [x] Persistence, messaging, and other I/O boundaries - [ ] Core pricing rules - [ ] Simple collection transformations - [ ] Referential transparency > **Explanation:** The shell should "do" things, while the pure core should "calculate" things. ### Why is introducing an atom too early often a mistake during refactoring? - [x] It can preserve the old imperative design instead of forcing you to extract the pure value transformations first - [ ] Atoms cannot hold maps - [ ] Atoms are deprecated - [ ] Atoms are only for macros > **Explanation:** State primitives are useful, but they should wrap a clean pure core rather than replace the work of extracting one.
Revised on Friday, April 24, 2026