Browse Clojure Foundations for Java Developers

Case Study: Refactoring Java Code

Refactor a mutable Java order model into Clojure value transformations, and see where state, rules, and side effects land afterward.

The fastest way to feel the Java-to-Clojure shift is to refactor a class that mixes mutable state and business rules into data plus pure transformations.

This case study does exactly that.

The Starting Java Shape

Suppose you inherit a Java Order class like this:

 1public class Order {
 2    private final String id;
 3    private final List<LineItem> items = new ArrayList<>();
 4    private BigDecimal discount = BigDecimal.ZERO;
 5    private Status status = Status.DRAFT;
 6
 7    public Order(String id) {
 8        this.id = id;
 9    }
10
11    public void addItem(LineItem item) {
12        items.add(item);
13    }
14
15    public void applyDiscount(BigDecimal amount) {
16        discount = amount;
17    }
18
19    public void submit() {
20        if (items.isEmpty()) {
21            throw new IllegalStateException("Cannot submit an empty order");
22        }
23        status = Status.SUBMITTED;
24    }
25}

This is not bad Java. It is a very normal object-oriented shape:

  • object owns mutable state
  • methods enforce invariants
  • callers change the object by sending commands

But the model hides several things together:

  • data structure
  • transition rules
  • mutation points

That makes reuse and testing heavier than they need to be.

Step 1: Represent The Order As Data

The first Clojure move is to model the order as a plain value:

1(defn new-order [id]
2  {:order/id id
3   :status   :draft
4   :discount 0M
5   :items    []})

This already changes the design conversation.

You no longer ask:

  • which object owns this behavior?

You ask:

  • what does a valid order value look like?

That shift is one of Clojure’s biggest simplifications.

Step 2: Turn Commands Into Pure Transitions

Each Java mutator becomes a function from old value to new value:

 1(defn add-item [order item]
 2  (update order :items conj item))
 3
 4(defn apply-discount [order amount]
 5  (assoc order :discount amount))
 6
 7(defn submit-order [order]
 8  (if (seq (:items order))
 9    (assoc order :status :submitted)
10    (throw (ex-info "Cannot submit an empty order"
11                    {:order/id (:order/id order)}))))

These functions do not mutate anything. They just return the next value.

That gives you a much cleaner testing story:

  • input value in
  • output value out
  • no fixture object lifecycle
  • no hidden state carried between method calls

Step 3: Compose The Workflow

The workflow that used to live across object mutation now becomes explicit value transformation:

1(-> (new-order "A-1001")
2    (add-item {:sku "A-1" :qty 2 :unit-price 15M})
3    (add-item {:sku "B-2" :qty 1 :unit-price 40M})
4    (apply-discount 5M)
5    submit-order)

This is easier to read because each step takes a value and returns a value.

Earlier versions can still be kept for debugging or comparison if you need them. No defensive copy policy is required.

Step 4: Put State Back Only Where It Belongs

Real applications still have current state somewhere.

If this order is evolving in memory, you can keep the identity in an atom while preserving pure business rules:

1(def current-order (atom (new-order "A-1001")))
2
3(swap! current-order add-item {:sku "A-1" :qty 2 :unit-price 15M})
4(swap! current-order apply-discount 5M)
5(swap! current-order submit-order)

The important split is:

  • add-item, apply-discount, and submit-order are still pure
  • the atom is only the state boundary

This is the “functional core, imperative shell” idea showing up again in a smaller form.

What Got Better

Concern Mutable Java object Clojure value model
Business rules Interleaved with mutation Pure transitions over data
Testing Often object-lifecycle oriented Plain input/output assertions
Debugging Need to inspect who changed the object Compare before and after values directly
Reuse Methods tied to object ownership Functions reusable anywhere the data shape fits

This is why many Java engineers describe Clojure as “less tangled” even before the system gets large.

What Did Not Disappear

The refactor does not eliminate:

  • validation
  • current state
  • error handling
  • persistence

It just moves them into clearer places.

That is the real win. Clojure is not removing complexity from the domain. It is removing accidental complexity from the implementation shape.

Knowledge Check

### What is the first major design change in the case study? - [x] Represent the order as plain data instead of a mutable object with behavior attached - [ ] Replace every method with a macro - [ ] Store each field in a separate atom immediately - [ ] Remove validation entirely > **Explanation:** The refactor starts by modeling the order as a value, which makes the later transition functions much simpler. ### In the Clojure version, what job does `submit-order` perform? - [x] It returns the next valid order value or raises an error if the transition is invalid - [ ] It mutates a shared object in place - [ ] It persists the order to the database automatically - [ ] It creates a Java class > **Explanation:** The core transition remains a pure rule over data, even if the application later stores or persists the result elsewhere. ### Why does the threaded workflow read more clearly than the Java mutator sequence? - [x] Each step transforms a value and passes the result forward explicitly - [ ] Because Clojure hides every intermediate value - [ ] Because immutable code never needs validation - [ ] Because object design is forbidden > **Explanation:** The pipeline makes the sequence of transformations visible instead of burying them in internal object state. ### What role does the atom play in the final step? - [x] It holds the changing identity while the business rules remain pure functions - [ ] It replaces all plain maps - [ ] It makes the functions mutable - [ ] It performs coordinated multi-order transactions > **Explanation:** The atom is the explicit state boundary. It does not change the fact that the core update functions are pure transitions.
Revised on Friday, April 24, 2026