Use refs and software transactional memory when several in-process identities must change together, and keep transactions pure because Clojure may retry them.
Refs are Clojure’s coordinated state tool. Where an atom protects one independent identity, refs are for multiple identities that must change together inside a dosync transaction.
For a Java engineer, refs are closer to database transaction thinking than lock thinking. You describe the changes, Clojure’s software transactional memory (STM) gives the transaction a consistent view, and the changes commit as one unit or retry.
| Scenario | Why refs may fit |
|---|---|
| Transfer between accounts | Debit and credit must commit together. |
| Move an item between queues | The item must not disappear or appear twice. |
| Maintain related indexes | Primary data and lookup index must agree. |
| Update one independent counter | Usually too much; use an atom. |
| Call external services during update | Bad fit inside the transaction. |
1(def checking (ref 500M))
2(def savings (ref 1000M))
3
4(defn transfer! [from to amt]
5 (dosync
6 (when (> amt @from)
7 (throw
8 (ex-info "low funds"
9 {:need amt
10 :have @from})))
11 (alter from - amt)
12 (alter to + amt)))
13
14(transfer! checking savings 125M)
The two balances are separate identities, but the invariant spans both. The debit and credit should not be reviewed as two unrelated atom updates.
| STM behavior | Engineering consequence |
|---|---|
| Reads see a consistent snapshot. | Your transaction reasons over a stable view. |
| Ref changes commit at one logical point. | Related facts do not become half-updated. |
| Conflicting transactions may retry. | Transaction bodies must avoid irreversible side effects. |
| Ref values should be immutable. | Persistent Clojure collections are the natural values to store. |
Keep transaction bodies focused on state transitions. Do not send emails, write to Kafka, charge cards, or mutate Java objects inside dosync.
| Function | Use |
|---|---|
alter |
Apply a function to a ref in a coordinated transaction. |
ref-set |
Set a ref to a new value inside a transaction. |
commute |
Apply a commutative update where ordering is less strict. |
ensure |
Protect a ref from change when you read it to validate another change. |
Most business code should start with alter. Reach for commute only when you can clearly explain why the update is safe to reorder, such as adding independent counts.
dosync small and side-effect free.