Model work as successive values instead of reassigning locals, and learn why this makes Clojure code easier to review and test.
Java developers often learn to express a calculation as a sequence of changing locals:
1BigDecimal total = subtotal;
2total = total.add(tax);
3total = total.subtract(discount);
4return total;
That style is normal in Java because reassignment is cheap and familiar.
In Clojure, the default move is different: compute the next value instead of reassigning the current one.
You are usually not asking:
You are asking:
That leads naturally to function composition, let, and collection transformations instead of stepwise mutation.
The practical difference is easier to see in a comparison.
| Habit | Java-style instinct | Clojure-style instinct |
|---|---|---|
| Running calculation | Reassign one local repeatedly | Bind intermediate values or compose expressions |
| Updating structured data | Mutate fields or setters | Return a new map or vector |
| Sharing code | Methods mutate collaborator state | Functions transform inputs into outputs |
| Debugging | Ask who changed the variable | Ask which expression produced this value |
That last point matters more than it first appears. Once locals stop changing, a lot of code review questions disappear.
Suppose you want to compute a final charge amount from a subtotal, tax rate, and discount.
Instead of mutating a running total, write the steps as value transformations:
1(defn final-charge [subtotal tax-rate discount]
2 (let [taxed (+ subtotal (* subtotal tax-rate))
3 discounted (- taxed discount)]
4 discounted))
Nothing is being reassigned here. taxed names one intermediate value, and discounted names the next one.
That is already clearer than a mutable running variable because each binding has one meaning.
For a pipeline of simple transformations, the threading macros can express the same idea more directly:
1(defn final-charge [subtotal tax-rate discount]
2 (-> subtotal
3 (+ (* subtotal tax-rate))
4 (- discount)))
This is still not mutation. Each step produces a new value that becomes the input to the next step.
When Java developers say Clojure “feels like data flowing through functions,” this is one of the places they are feeling it.
Clojure does let you shadow a local name in a later binding:
1(let [total 100M
2 total (+ total 13M)
3 total (- total 5M)]
4 total)
This works, and it is still not mutation. Each total is a new binding that can see the previous one.
But use this style carefully. A small amount of shadowing can be tidy; too much starts to imitate mutable code without any of the clarity benefits.
In many cases, better names are clearer:
1(let [subtotal 100M
2 taxed (+ subtotal 13M)
3 final (- taxed 5M)]
4 final)
Avoiding reassignment improves more than aesthetics.
That is a better default for most application code.
Avoiding reassignment does not mean pretending state does not exist.
It means:
That separation is the core discipline.