Choose Clojure atoms, refs, and agents by separating immutable values from changing identities and deciding how state updates should be coordinated.
Java concurrency often starts with threads, executors, locks, atomics, and concurrent collections. Clojure starts from a different default: immutable values are safe to share, and changing identities are managed explicitly.
The first question is not “which lock do I need?” The first question is:
What identity changes over time, and how should updates to that identity be coordinated?
| Primitive | Best fit | Timing | Coordination |
|---|---|---|---|
| Atom | One independent identity | Synchronous | Compare-and-set retry |
| Ref | Multiple coordinated identities | Synchronous transaction | Software transactional memory |
| Agent | One independent identity | Asynchronous | Queued actions |
flowchart TD
A["Changing state?"] --> B{"Multiple related identities?"}
B -->|Yes| C["ref + dosync"]
B -->|No| D{"Caller needs result now?"}
D -->|Yes| E["atom"]
D -->|No| F["agent"]
This is a first-pass selection rule, not a complete concurrency architecture.
Use an atom when one identity changes independently and the caller should see the updated 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}
swap! may retry the update function under contention. Keep the update function pure. Do not put logging, sending, database writes, or “must happen once” effects inside it.
Use refs when multiple identities must change together:
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)))
Refs are for coordinated in-memory state. They are not a replacement for database transactions.
Use an agent when updates can be queued and the caller does not need the updated value immediately.
1(def audit-log (agent []))
2
3(send audit-log conj {:event :login :user-id 42})
Agents are useful for serialized asynchronous state changes around one identity. They are not a general distributed queue or actor system.
| Need | More likely tool |
|---|---|
| Run a task later | future, executor, scheduler, or queue |
| Coordinate streaming messages | core.async, queue, broker, or reactive library |
| Persist business transactions | Database transaction |
| Share immutable data | No reference type needed |
Clojure’s reference types manage state over time. Do not use them just because a task is asynchronous.
Ask these questions:
That checklist catches most early concurrency design mistakes.