Browse Learn Clojure Foundations as a Java Developer

Choose Clojure State Tools

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:

  1. Model business facts as immutable data.
  2. Write pure functions from old value to new value.
  3. Put the changing identity behind one explicit reference.
  4. Keep I/O and irreversible effects outside retryable update functions.

Decision Table

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.

The Shared Shape

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.

What Not to Do

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 Translation

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.

Knowledge Check

### Which state tool fits one independent value that must update synchronously? - [x] Atom - [ ] Ref - [ ] Agent - [ ] Dynamic Var > **Explanation:** An atom is for independent synchronous state. It does not coordinate multiple identities and does not queue asynchronous actions. ### When should refs and STM be considered? - [x] When several changing identities must be updated consistently as one transaction. - [ ] Whenever a single counter is incremented. - [ ] Whenever a namespace defines a function. - [ ] Whenever a task should run later in a thread pool. > **Explanation:** Refs are for coordinated synchronous changes across multiple references. A single independent value usually fits an atom better. ### Why is a pure transition function valuable across these tools? - [x] It can be tested directly and safely retried or scheduled by the state mechanism. - [ ] It prevents all memory allocation. - [ ] It turns every update into a database transaction. - [ ] It removes the need to pick a reference type. > **Explanation:** Clojure state tools work best when the update is a function from old immutable value to new immutable value. The reference type handles coordination.
Revised on Saturday, May 23, 2026