Common mistakes with Clojure state primitives, including choosing the wrong coordination model and hiding effects in retrying updates.
Misusing atoms, refs, and agents is a concurrency anti-pattern because these primitives do not solve the same problem. They all manage state, but they express different coordination models. Treating them as interchangeable usually leads to retries in the wrong place, consistency failures, or async behavior that the caller never really intended.
The question is not “Which mutable-looking thing should I use?” The question is “What consistency and timing contract does this state change actually need?”
The rough split is:
Most misuse comes from ignoring that split.
Atoms are excellent when one piece of state can change independently. They are weak when several values must stay consistent together.
Weak:
1(def account-a (atom 100))
2(def account-b (atom 200))
3
4(defn transfer! [amount]
5 (swap! account-a - amount)
6 (swap! account-b + amount))
This looks simple, but it has no transactional guarantee across both accounts. If something fails or interleaves between updates, the system can observe a partially applied transfer.
That is a ref problem, not an atom problem:
1(def account-a (ref 100))
2(def account-b (ref 200))
3
4(defn transfer! [amount]
5 (dosync
6 (alter account-a - amount)
7 (alter account-b + amount)))
The anti-pattern is choosing the cheapest primitive when the consistency contract is richer than “update one thing.”
swap!This is one of the most important atom mistakes.
1(swap! state
2 (fn [s]
3 (send-email! s)
4 (assoc s :emailed? true)))
swap! may retry its function. That means the side effect may run more than once.
The state transition function passed to swap! should usually be pure. Compute the new value there, then perform side effects outside the retrying update path if needed.
The anti-pattern is hiding non-idempotent effects inside an update function that the runtime may invoke repeatedly.
Refs are powerful because STM coordinates several changes together. That power is overhead when the state is actually independent.
Weak:
1(def page-view-count (ref 0))
2
3(defn record-view! []
4 (dosync
5 (alter page-view-count inc)))
If this counter does not need transactional coordination with other refs, an atom is often the better fit:
1(def page-view-count (atom 0))
The anti-pattern is paying STM complexity for state that never needed coordinated transactions in the first place.
Agents are for asynchronous state changes. That makes them useful for:
They are a poor fit when callers need the new value immediately.
Weak:
1(def balance (agent 100))
2
3(send balance + 50)
4@balance
That deref may observe the old value if the action has not run yet.
The anti-pattern is choosing agents for a workflow that is actually synchronous in business terms.
Sometimes teams put a huge application map in one atom because it feels simple.
That can work for very small systems, but it becomes problematic when:
This is often a boundary smell rather than a primitive smell. The state model is too centralized.
The fix may be:
Refs are not just “safer atoms.” They participate in transactions. That means you need to think about:
dosyncDoing slow I/O or unpredictable side effects inside dosync is a bad idea for the same reason side effects inside swap! are risky: retries and timing become messy.
Keep transaction bodies focused on coordinated in-memory state transitions.
Agents are often introduced for “background work” and then forgotten. That creates its own failures:
An agent is not just a convenient mailbox. It is an asynchronous execution surface that needs error handling and operational awareness.
flowchart TD
A["Need state change"] --> B{"One independent value?"}
B -->|Yes| C["Atom likely fits"]
B -->|No| D{"Several values must remain consistent together?"}
D -->|Yes| E["Refs with dosync may fit"]
D -->|No| F{"State change can be asynchronous?"}
F -->|Yes| G["Agent may fit"]
F -->|No| H["Reconsider the state model or coordination boundary"]
This is less about memorizing primitives and more about choosing the correct consistency and timing contract.
swap! functions pureswap!.