Learn how to choose between atoms, refs, and agents by the coordination and timing guarantees your Clojure state actually needs.
Reference type: A Clojure construct that gives controlled access to changing state while encouraging the value itself to remain an immutable data structure.
Atoms, refs, and agents all manage change, but they solve different concurrency problems. The important question is not “which one is more advanced?” It is “does this state need to be coordinated with other state, and does the update need to happen synchronously or asynchronously?”
The official atoms reference describes atoms as shared, synchronous, independent state. That is the core idea: one location, updated atomically, with no coordination across multiple references.
1(def counter (atom 0))
2
3(swap! counter inc)
4@counter
5;; => 1
Atoms are a good fit when:
The critical semantic detail is that swap! may retry. That means the function you pass to swap! must be free of side effects, because it may be invoked more than once.
Refs exist for the harder case: multiple pieces of state that must change together.
1(def checking (ref 100))
2(def savings (ref 200))
3
4(dosync
5 (alter checking - 20)
6 (alter savings + 20))
The refs and transactions reference emphasizes atomic, consistent, and isolated updates. In practice, that means:
dosync commit together or not at allRefs are the right choice when the correctness rule spans more than one state location.
The official docs also call out commute and ensure as important STM tools. Those matter once you move beyond toy examples, because not every coordinated update has the same contention pattern.
Agents are different again. The official agents reference describes them as shared access to mutable state where changes happen independently and asynchronously.
1(def log-lines (agent []))
2
3(send log-lines conj "started")
4@log-lines
The key difference from atoms is timing. send returns immediately, and the action runs later in an agent thread pool. That makes agents appropriate when:
The docs also preserve the classic distinction:
send for CPU-bound actionssend-off for actions that may block on I/OYou can usually choose among these three by asking two questions:
flowchart TD
A["Need changing state"] --> B{"Must coordinate with other references?"}
B -- Yes --> C["Use refs with STM"]
B -- No --> D{"Must update synchronously?"}
D -- Yes --> E["Use an atom"]
D -- No --> F["Use an agent"]
That is much more reliable than picking based on syntax preference.
Two of these constructs demand special care:
swap! functions may run more than onceThat means effects like:
should not live casually inside those update functions.
Agents are different: their actions are asynchronous, but failures are cached in the agent. The official docs note that agent errors can be inspected with agent-error and cleared via restart-agent. Also, agent thread pools keep the JVM alive until shutdown-agents is called.
The official refs docs are unusually direct here: the values placed in refs must be, or be considered, immutable. The same design assumption applies across all of Clojure’s reference types. The reference cell is the mutable slot; the value inside should still be treated as immutable data.
That is what makes retries, snapshots, and safe sharing practical.
swap! functions or dosync transactionsswap! and STM transactions can retry, so their functions must avoid side effects.