What vars and bindings are in Clojure, how `def`, `let`, and `binding` differ, and where Java developers usually get confused.
Var and binding are two terms Java developers often mix together early in Clojure. The confusion is understandable because Java uses one word—“variable”—for several ideas that Clojure keeps more separate.
The short version is:
That distinction matters because it helps you read Clojure code correctly.
When you write def, you usually create a var in the current namespace.
1(def tax-rate 0.13)
2
3(defn total-with-tax [subtotal]
4 (* subtotal (+ 1 tax-rate)))
Here, tax-rate and total-with-tax are vars in the namespace. A var is not just a raw value. It is a named slot that can hold or point to a value.
That is why vars support behaviors such as metadata, dynamic rebinding, and interactive redefinition in the REPL.
Vars are one reason REPL-driven development works so well in Clojure. You can redefine a function var while the program is running and then call the new version immediately.
For Java developers, that feels very different from the normal compile-run cycle.
A binding is the association between a name and a value in a given scope.
The most common example is let.
1(defn invoice-total [subtotal discount]
2 (let [discounted (- subtotal discount)
3 tax (* discounted 0.13)]
4 (+ discounted tax)))
Inside the let, discounted and tax are local bindings. They are not namespace vars. They exist only within that lexical scope.
That is the important split:
def creates namespace-level varslet creates local bindingslet names as mini mutable variablesA common Java habit is to read local names as values that might be reassigned later. In Clojure, let bindings are normally fixed for that scope.
You create a new binding when you need a new name or a nested scope. You do not usually “update” a let binding in place.
That pushes code toward clearer data flow:
Some vars are marked dynamic and can be temporarily rebound with binding.
1(def ^:dynamic *currency* "USD")
2
3(defn format-price [amount]
4 (str *currency* " " amount))
5
6(binding [*currency* "CAD"]
7 (format-price 10))
8;; => "CAD 10"
This does not mutate the global var permanently. It creates a temporary dynamic binding for the duration of that scope.
That is useful for context-like values such as logging settings, print behavior, or request-scoped configuration, but it should be used deliberately.
A rough comparison is:
def gives you a namespace-level named reference, somewhat like a named static slot, but more dynamiclet gives you lexical local bindings, closer to local names in an expression-oriented languagebinding temporarily rebinds a dynamic var for a scopeThe important warning is that none of these are exactly the same as a mutable Java local variable or field.
If you see (def x 10), the var is x; the value currently held by that var is 10. Keeping that distinction in mind makes REPL behavior easier to understand.
Dynamic vars are for scoped context, not as a default way to manage changing business data.
Just because def is easy does not mean every shared name should be global. Prefer passing values through functions unless a namespace-level var truly makes sense.
When reading code, ask two questions:
Those two questions remove a lot of early confusion.