Browse Learn Clojure Foundations as a Java Developer

Understand Vars and Dynamic Bindings

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.

Vars in Ordinary Code

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.

Dynamic Vars

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.

Thread Context and Binding Conveyance

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.

What Vars Are Not

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.

REPL Redefinition Is a Tool, Not a State Model

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.

Knowledge Check

### What does `defn` create at namespace scope? - [x] A Var whose root binding holds the function. - [ ] A Java local variable. - [ ] A ref transaction. - [ ] An agent action. > **Explanation:** Namespace symbols resolve through Vars. `defn` creates and binds a Var to a function value. ### When is a dynamic Var reasonable? - [x] For narrow scoped context where explicit arguments would make APIs worse. - [ ] For every changing domain value. - [ ] For replacing all function parameters. - [ ] For coordinating several bank-account balances. > **Explanation:** Dynamic Vars are powerful but should remain narrow. Ordinary data and dependencies are usually clearer as explicit arguments. ### Why should Java engineers not treat Vars as normal mutable variables? - [x] Vars are namespace references or dynamic bindings, not assignable local variables. - [ ] Vars can only hold strings. - [ ] Vars are stored outside the JVM. - [ ] Vars cannot be read from functions. > **Explanation:** Clojure local bindings are immutable. Vars serve namespace and dynamic-binding roles, so using them as general mutable storage creates hidden coupling.
Revised on Saturday, May 23, 2026