Distinguish namespace vars from local bindings, and learn where Clojure's immutability guarantee actually applies.
This page exists because the phrase “Clojure is immutable” is true in one sense and misleading in another.
Clojure strongly prefers immutable values and immutable local bindings.
But names in a namespace are usually vars, and vars can be redefined or, when marked dynamic, rebound per thread.
That is the real model you need if you want def, defn, let, and explicit state references to make sense together.
The most useful comparison is short:
| Kind of name | Created by | Scope | Normal use |
|---|---|---|---|
| Namespace var | def, defn |
Current namespace, reachable via its qualified name | Public API functions, shared top-level values |
| Local binding | let, function parameters |
Only inside the enclosing expression or function body | Intermediate calculation steps |
Example:
1(def tax-rate 0.13M)
2
3(defn invoice-total [subtotal]
4 (let [tax (* subtotal tax-rate)]
5 (+ subtotal tax)))
Here:
tax-rate is a namespace varsubtotal is a parameter bindingtax is a local bindingThose names are not equivalent just because they all look like symbols in code.
The local bindings are immutable. Once subtotal or tax is bound inside that call, you do not reassign them.
The values are immutable too. A number or map does not change in place.
But the namespace var tax-rate can later be redefined at the REPL or in source:
1(def tax-rate 0.15M)
That does not mean Clojure abandoned immutability. It means a var now refers to a different root value.
This is why saying “def creates an immutable variable” is not precise enough. The value is immutable. The var is a named reference in the namespace.
For Java developers, this can feel strange until you connect it to the REPL.
Because functions are stored in vars:
That dynamic workflow is one of Clojure’s biggest productivity gains, but it only feels safe if most actual business logic still uses immutable data and local bindings.
Another common migration mistake is to treat top-level vars as the normal place for changing application state.
That usually leads to brittle code because:
If state should change during program execution in a controlled way, use an explicit reference type such as an atom or ref instead of repeatedly redefining ordinary vars.
1(def current-orders (atom {}))
That makes the changing identity explicit. It is much clearer than pretending repeated def calls are your state model.
Use these rules in code review:
def and defn introduce namespace names.let and function parameters introduce local names.If you keep those five rules straight, most beginner confusion around scope and immutability disappears.