Browse Clojure Foundations for Java Developers

Avoiding Reassignment

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.

The Basic Mental Shift

You are usually not asking:

  • which variable should change next?

You are asking:

  • what value should this expression produce next?

That leads naturally to function composition, let, and collection transformations instead of stepwise mutation.

Reassignment Versus Successive Values

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.

A Direct Translation

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.

Threading Often Reads Better Than Reassignment

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.

Avoid Fake Mutation With Shadowing

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)

Why This Helps On Real Teams

Avoiding reassignment improves more than aesthetics.

  • Tests get simpler because functions depend on explicit inputs.
  • Refactors get safer because earlier bindings do not change meaning later.
  • Parallel reasoning gets easier because immutable values can be shared freely.
  • Code reviews move from “where did this variable change?” to “is this transformation correct?”

That is a better default for most application code.

When You Really Need Changing State

Avoiding reassignment does not mean pretending state does not exist.

It means:

  • use local bindings for ordinary calculation
  • use collection operations for transforming data
  • use explicit reference types such as atoms when an identity truly changes over time

That separation is the core discipline.

Knowledge Check

### What is the main Clojure replacement for repeatedly reassigning a running local? - [x] Compute successive values through expressions, bindings, or function composition - [ ] Use `def` inside the function body - [ ] Mutate maps in place - [ ] Store every step in a global var > **Explanation:** Clojure code usually models intermediate results as new values rather than changes to one mutable local variable. ### Why does avoiding reassignment often improve code review? - [x] Each binding keeps one meaning, so reviewers can focus on transformations instead of tracking variable changes - [ ] It guarantees the code runs faster - [ ] It removes the need for tests - [ ] It makes all functions recursive > **Explanation:** Stable bindings reduce mental bookkeeping. That makes the logic easier to follow and harder to misread. ### What is true about shadowing the same local name in a `let`? - [x] It creates a new binding, not in-place mutation - [ ] It changes the old binding directly - [ ] It is invalid Clojure - [ ] It always updates a namespace var > **Explanation:** Later `let` bindings can see earlier ones, but each name still refers to a new binding rather than a mutated variable. ### When should you reach for a reference type like an atom? - [x] When one identity must change over time beyond a single local calculation - [ ] Whenever you need a temporary variable - [ ] To replace every `let` - [ ] To make numbers mutable > **Explanation:** Atoms are for explicit state over time, not for ordinary local computation.
Revised on Friday, April 24, 2026