Convert loops, mutable accumulators, conditionals, and staged object updates into Clojure pipelines that transform immutable values while preserving behavior and edge cases.
Replacing imperative Java constructs does not mean replacing every loop with recursion. In production Clojure, the usual replacements are collection functions, sequence pipelines, reduce, for, transducers when needed, and small pure helper functions.
The migration goal is behavioral clarity. If the Java code mixes iteration, branching, mutation, and I/O, first separate those concerns. Then choose the Clojure construct that makes the transformation easiest to test.
Java code often hides a data transformation inside mutable local variables.
1Map<String, BigDecimal> totalsBySku(List<OrderLine> lines) {
2 Map<String, BigDecimal> totals = new HashMap<>();
3 for (OrderLine line : lines) {
4 if (line.isBillable()) {
5 BigDecimal current = totals.getOrDefault(line.sku(), BigDecimal.ZERO);
6 totals.put(line.sku(), current.add(line.extendedPrice()));
7 }
8 }
9 return totals;
10}
This code has a simple purpose: keep billable lines, group them by SKU, and sum money. The Clojure version should make that purpose visible.
1(defn totals-by-sku [lines]
2 (reduce (fn [totals {:line/keys [sku extended-price]}]
3 (update totals sku (fnil + 0M) extended-price))
4 {}
5 (filter :line/billable? lines)))
The mutable HashMap became an accumulated immutable map. Each update returns a new value that shares structure with the old one, so callers never observe a partially updated collection.
| Java construct | Common Clojure replacement | Use when |
|---|---|---|
for loop that transforms every item |
map |
Each input produces one output. |
for loop that filters items |
filter or remove |
Items are kept or discarded by a predicate. |
| Mutable accumulator | reduce |
Many inputs become one summary value. |
| Nested loops that produce combinations | for comprehension |
The output is a sequence of derived values. |
break or early return |
some, reduced, or a smaller function |
The computation can stop after a match. |
| Staged object mutation | Threading macros plus immutable updates | Each step returns a new value. |
Do not make recursion your default answer. Use loop and recur when you need explicit iterative control, not when map, filter, or reduce already expresses the operation.
A safe migration keeps the Java behavior visible while changing the implementation shape.
That sequence prevents a common mistake: rewriting the loop and changing the business rule at the same time.
Imperative code often nests if blocks until the real cases are hard to see.
1String riskBand(Customer c) {
2 if (!c.isActive()) {
3 return "inactive";
4 }
5 if (c.balance().compareTo(new BigDecimal("10000")) > 0) {
6 return "high";
7 }
8 if (c.missedPayments() > 0) {
9 return "watch";
10 }
11 return "standard";
12}
Clojure’s cond makes ordered business rules explicit.
1(defn risk-band [{:customer/keys [active? balance missed-payments]}]
2 (cond
3 (not active?) :inactive
4 (> balance 10000M) :high
5 (pos? missed-payments) :watch
6 :else :standard))
The important part is not that cond is shorter. The important part is that each case is now a value-level rule with direct test inputs.
Java often builds a result through a sequence of assignments. Clojure can express the same staged transformation without mutating a local object.
1(defn normalized-order [order]
2 (-> order
3 (update :order/email clojure.string/lower-case)
4 (update :order/items #(remove :item/cancelled? %))
5 (assoc :order/status :ready-for-validation)))
Use -> when the value flows through the first argument position, and ->> when a collection flows through the last argument position. Threading macros should clarify the pipeline. If they hide too much, split the transformation into named functions.
| Trap | Better approach |
|---|---|
| Replacing every loop with manual recursion | Prefer collection functions and reduce; reserve loop/recur for explicit control. |
| Keeping Java-style mutable accumulators in atoms | Use atoms for real shared state, not local loop variables. |
| Combining filtering, grouping, validation, and I/O in one function | Separate pure transformations from effectful boundaries. |
| Assuming lazy sequences always improve performance | Measure; realize sequences when side effects or resource lifetimes require it. |
| Changing data shape while changing logic | Validate one change at a time with fixtures. |
reduce.if/else decision into a cond with explicit ordered rules.-> plus assoc or update.map, filter, reduce, for, cond, and threading macros cover most loop-and-mutation rewrites.