Compare atoms, refs, agents, and Vars from a Java engineer's perspective so each changing identity has the right coordination model instead of an accidental lock or global variable.
Clojure’s state tools are not interchangeable names for “variable.” Each one answers a different coordination question. Before choosing one, write down the identity that changes, the value it holds, and whether the update must coordinate with other identities.
The intended pattern is usually:
| Question | Prefer | Why |
|---|---|---|
| Can one identity update independently and synchronously? | Atom | swap! applies a pure function atomically to the current value. |
| Must several identities change as one unit? | Refs and STM | dosync coordinates several refs with transaction semantics. |
| Can one identity process updates asynchronously? | Agent | send or send-off queues functions that produce the next state. |
| Is this a namespace definition or scoped context? | Var | Vars hold namespace bindings and can be dynamically rebound when marked dynamic. |
| Is the value durable or shared across processes? | Database or external store | Clojure references are in-process JVM coordination tools. |
All four tools preserve the same high-level idea: values are stable, references identify places where a current value can be observed or replaced.
1;; Value: safe to share.
2{:count 41}
3
4;; Reference: the identity whose value can change.
5(def counter (atom {:count 41}))
6
7;; Transition: old value -> new value.
8(swap! counter update :count inc)
The code should make it obvious where state changes. If a Java design hides mutation behind many methods on many objects, a Clojure design usually benefits from moving the rule into pure functions and narrowing the mutable reference.
| Mistake | Better move |
|---|---|
| Put every map in its own atom. | Group state by invariant and update boundary. |
| Use refs because “transactions sound safer.” | Use refs only when several identities must coordinate. |
| Use agents as generic background threads. | Use agents when a state value should receive serialized async actions. |
| Use dynamic Vars for ordinary parameters. | Pass values explicitly unless scoped context is genuinely cross-cutting. |
Perform I/O inside swap!, dosync, or agent state functions casually. |
Keep transition functions pure or isolate effects deliberately. |
| Java mental model | Clojure model |
|---|---|
| Mutable field | Immutable value plus explicit reference if identity changes. |
AtomicReference<T> |
Atom holding an immutable value. |
| Lock guarding related fields | One immutable aggregate, or refs inside dosync. |
| Worker object with a queue | Agent, channel consumer, or Java executor at a boundary. |
| Thread-local context | Dynamic Var only when explicit arguments are worse. |
The practical rule is simple: choose the tool that describes the coordination problem, not the one that looks most familiar from Java.