Understand Clojure Vars as namespace bindings, REPL-redefinable roots, and carefully scoped dynamic context rather than ordinary mutable Java variables.
A Var is the reference behind a namespace-level definition. When you write def or defn, Clojure interns a Var in the current namespace. That is why functions can be redefined at the REPL: the symbol resolves through a Var whose root binding can point to a newer value.
Vars are not Java local variables. You do not assign to let bindings or function parameters. Use Vars for namespace definitions and, more rarely, for scoped dynamic context.
1(ns billing.tax)
2
3(def default-rate 0.13M)
4
5(defn add-tax [amount]
6 (+ amount (* amount default-rate)))
default-rate and add-tax are namespace Vars. In normal application design, you should still prefer passing configuration explicitly rather than relying on global state.
1(defn add-tax [rate amount]
2 (+ amount (* amount rate)))
The second version is easier to test and easier to run in parallel because the dependency is visible.
A dynamic Var can be rebound for the current thread using binding. The naming convention is earmuffs: *name*.
1(def ^:dynamic *tenant-id* nil)
2
3(defn current-tenant []
4 *tenant-id*)
5
6(binding [*tenant-id* "acme"]
7 (current-tenant))
8;; => "acme"
Use dynamic Vars sparingly for cross-cutting context such as print settings, test fixtures, tracing context, or request context where passing the value through every function would make APIs worse. Do not use them as a replacement for ordinary function parameters.
Dynamic bindings are per-thread. Some Clojure concurrency functions convey dynamic bindings to work they start, including futures and agent actions, but you should still design as if hidden context is a cost.
1(def ^:dynamic *request-id* nil)
2
3(binding [*request-id* "req-42"]
4 @(future *request-id*))
5;; => "req-42"
This is useful, but it can surprise Java engineers expecting ordinary ThreadLocal behavior. Keep dynamic scope narrow and document it.
| Misuse | Better design |
|---|---|
| Global mutable application state | Atom, ref, database, or explicit system map. |
| Hidden function dependency | Pass an argument or dependency map. |
| Per-request business data everywhere | Pass request data unless dynamic scope is clearly justified. |
| Test monkey-patching as a default | Prefer dependency injection; use with-redefs only for narrow tests. |
Clojure’s REPL workflow depends on Vars. Redefining a function updates the Var so subsequent calls can use the new definition. That is excellent for development, but production state should not depend on redefining Vars at runtime.