Use dosync, alter, and ref-set to express coordinated ref updates, and design each transaction around one invariant that must commit atomically.
Refs can be read anywhere, but they can only be changed inside a dosync transaction. That rule is a feature: it makes coordinated state changes visible in the source code.
Start by naming the invariant. If the invariant does not span more than one identity, an atom is usually simpler.
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 {:amount amt})))
10 (alter from - amt)
11 (alter to + amt)))
The transaction protects the invariant that money is not lost between the debit and credit. A Java implementation might need careful lock ordering across two account objects; the Clojure version makes the transaction boundary explicit.
| Operation | Use it when | Notes |
|---|---|---|
alter |
The new value depends on the old value. | Default choice for most business updates. |
ref-set |
You already have the complete replacement value. | Must still run inside dosync. |
commute |
The update is safe to reorder. | Use only when commutativity is easy to prove. |
ensure |
You read a ref and need it unchanged before commit. | Useful for validation dependencies. |
Most code should start with alter. Do not use commute just because it sounds faster; only use it when update order cannot affect correctness.
| Boundary mistake | Better design |
|---|---|
| One huge transaction for unrelated work | Split by invariant. |
| Separate atoms for facts that must agree | Use one aggregate atom or refs in dosync. |
| I/O inside the transaction | Commit data first, perform I/O after. |
| Mutable Java objects inside refs | Store immutable values or isolate Java mutation outside STM. |
The transaction should be small enough to review. A reviewer should be able to answer: what invariant is protected, which refs participate, and what work can retry?
alter, ref-set, commute, or ensure appears inside dosync.