Choose atoms, refs (STM), or agents based on whether state updates must be coordinated and whether they are synchronous.
Java developers often learn concurrency through a toolbox of lower-level choices:
Clojure changes the starting point. You still care about throughput and correctness, but the first question becomes:
What kind of changing identity do I have, and how should updates to it be coordinated?
The core reference types are easier to choose once you separate two concerns:
The diagram below is a useful first-pass decision rule.
flowchart TD
Coord{"Do updates need coordination across multiple identities?"}
Coord -- "Yes" --> Ref["ref + dosync (coordinated transactional state)"]
Coord -- "No" --> Async{"Should the caller wait for the new state now?"}
Async -- "Yes" --> Atom["atom (independent synchronous updates)"]
Async -- "No" --> Agent["agent (independent asynchronous actions)"]
What to notice:
atom is the default for a single independent piece of stateref is for coordinated in-memory transactionsagent is for queued asynchronous actions against a single identityUse an atom when one identity changes independently and callers want the new value immediately.
1(def stats (atom {:requests 0 :errors 0}))
2
3(defn record-request [state]
4 (update state :requests inc))
5
6(swap! stats record-request)
7@stats
8;; => {:requests 1, :errors 0}
This is a good fit for:
swap! uses a compare-and-set retry loop, so the update function must be safe to run more than once. Keep it pure.
Java mental model: roughly like
AtomicReference<T>with an update function, but used pervasively with immutable maps and vectors.
Use ref when multiple identities must change together or not at all.
1(def checking (ref 1000))
2(def savings (ref 2000))
3
4(defn transfer! [amount]
5 (dosync
6 (alter checking - amount)
7 (alter savings + amount)))
This is not a replacement for a database transaction. It is a way to coordinate in-memory state safely.
Refs are appropriate when:
Like atom updates, STM transactions may retry. Keep side effects out of dosync.
Use an agent when state changes should happen asynchronously and the caller does not need the new value immediately.
1(def audit-log (agent []))
2
3(send audit-log conj {:event :login :user-id 42})
Agents are useful for:
They are not a substitute for queues, streams, or a full actor system. They are a focused tool for asynchronous state transitions around one identity.
New Clojure developers sometimes confuse reference types with task constructs. They are related, but not identical.
atom, ref, and agent manage state over timefuture, promise, and core.async help with asynchronous work and coordinationThat distinction matters. Choosing the wrong tool is often a sign that the real question is about work scheduling, not shared state.
Ask these questions in order:
If you answer:
atomrefagentThat rule will get you surprisingly far in ordinary application code.