Use atoms, refs, and agents deliberately, while keeping state transitions pure and effect boundaries explicit.
Once you separate value from identity, state changes become easier to reason about.
The value itself stays immutable. The changing identity points to a different value over time.
That is the core Clojure model, and it leads to a better question than “How do I mutate this object safely?”
The better question is:
Which reference type should own this identity, and can the transition itself stay pure?
You do not need a different reference type for every field. Start with the smallest tool that matches the coordination problem.
| Situation | Clojure tool | Why it fits |
|---|---|---|
| One independent identity, updated synchronously | atom |
Simple atomic updates to a single current value |
| Multiple in-memory identities must change together | ref + dosync |
Coordinated transactional updates |
| One identity should update asynchronously | agent |
Queued actions without blocking the caller |
If the real problem is a database transaction, Kafka delivery, or coordination across processes, these tools do not magically replace that infrastructure. They manage in-process state.
An atom is often the first tool you need:
1(def seat-map
2 (atom {:show/id "evening-1"
3 :reserved-seats #{}}))
4
5(defn reserve-seat [show seat-id]
6 (update show :reserved-seats conj seat-id))
7
8(swap! seat-map reserve-seat "A-12")
The important split is:
reserve-seat decides the next valueswap! installs that value as the new stateThat is cleaner than burying the business rule inside mutable object methods.
It also matters because swap! may retry under contention. The update function can run more than once, so keep it free of logging, database calls, and other effects.
Refs are for coordinated in-memory change, not as a fancier atom.
1(def inventory (ref {"SKU-1" 10}))
2(def reservations (ref {}))
3
4(defn reserve-stock! [order-id sku qty]
5 (dosync
6 (let [available (get @inventory sku 0)]
7 (when (< available qty)
8 (throw (ex-info "Insufficient inventory"
9 {:order/id order-id
10 :sku sku
11 :requested qty
12 :available available})))
13 (alter inventory update sku - qty)
14 (alter reservations assoc order-id {:sku sku :qty qty}))))
This is a good fit because both identities must change together. Either the reservation exists and inventory decreases, or neither change should happen.
Two cautions matter here:
dosync blocks free of I/O because transactions may retrySometimes the caller should not wait for a state update to finish. That is where an agent can fit.
1(def audit-log (agent []))
2
3(defn append-audit [entries event]
4 (conj entries event))
5
6(send audit-log append-audit
7 {:event/type :order/submitted
8 :order/id 42})
This is useful for background accumulation or other asynchronous updates to one identity.
Agents are not a general queueing platform, and they are not a replacement for a durable messaging system. They are a focused tool for asynchronous state transitions inside the process.
| Mistake | Why it causes trouble | Better move |
|---|---|---|
Putting side effects inside swap! or dosync |
Retries can duplicate the side effect | Keep the update function pure and do effects before or after the state change |
| Reaching for shared state too early | Extra identities increase coordination and testing cost | Pass values through pure functions until a real shared identity is required |
| Using a ref for a single independent value | STM adds coordination machinery you do not need | Start with an atom |
| Treating refs as distributed transactions | Refs only coordinate in-process identities | Use the right external system for cross-process consistency |
If this page feels more operational than philosophical, that is intentional. State is where many Java habits either translate cleanly or become expensive.