Handling State with Atoms, Refs, and Agents

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?”

Atoms for Independent, Synchronous State

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 state stands alone
  • updates are local and immediate
  • no transaction across other references is required

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 for Coordinated Transactions

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:

  • changes inside dosync commit together or not at all
  • transactions can retry automatically
  • side effects inside transactions are a mistake

Refs 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 for Independent, Asynchronous State

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 state is independent
  • eventual application of the change is acceptable
  • the work should happen off-thread

The docs also preserve the classic distinction:

  • send for CPU-bound actions
  • send-off for actions that may block on I/O

The Real Decision Framework

You can usually choose among these three by asking two questions:

  1. does this state need to coordinate with other state?
  2. does the update need to be applied synchronously?
    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.

Avoid Side Effects in Retriable Contexts

Two of these constructs demand special care:

  • swap! functions may run more than once
  • ref transactions may retry automatically

That means effects like:

  • sending emails
  • writing logs directly
  • calling external services
  • mutating host objects

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.

Values Should Still Be Immutable

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.

Common Mistakes

  • using an atom when two state locations must stay consistent together
  • putting side effects inside swap! functions or dosync transactions
  • using refs when the state is actually independent and simpler with an atom
  • using agents when callers really need immediate consistency
  • forgetting to handle agent failures or shutdown in long-running apps

Key Takeaways

  • Atoms are for independent synchronous state.
  • Refs are for coordinated transactional state.
  • Agents are for independent asynchronous state.
  • swap! and STM transactions can retry, so their functions must avoid side effects.
  • The values inside these references should still be immutable data.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026