Coordinated, Synchronous State: Why Refs Exist
An atom is fine when you have one independent value to manage, but many real-world updates touch more than one piece of state that must stay consistent — the classic example is transferring money between two bank accounts, where you never want a state where money has left one account but not yet arrived in the other. Refs, combined with Clojure's software transactional memory (STM), solve exactly this: you wrap a group of ref updates in a dosync block, and Clojure guarantees the whole group commits atomically, consistently, and in isolation from other concurrent transactions.
Cricket analogy: Like a DRS review that must jointly confirm both 'out' and the runs scored together — you can't accept one change without the other; refs coordinate multiple pieces of state, like two linked totals, atomically the way a review changes the wicket and the scoreboard consistently.
Clojure's STM uses multiversion concurrency control (MVCC), not locks: each transaction works against a consistent in-transaction snapshot and only commits if no other transaction wrote to the same refs in the meantime, otherwise it transparently retries.
Working with dosync, alter, and ref-set
You create a ref with (ref initial-value), the same way you'd create an atom. Inside a dosync block, alter applies a function to a ref's current value — the ref equivalent of swap! — while ref-set replaces the value outright, the ref equivalent of reset!. Any attempt to call alter, ref-set, or commute outside of a dosync transaction throws an exception, since Clojure needs the transactional context to guarantee atomicity, consistency, and isolation across every ref touched.
Cricket analogy: Like a scorer using 'add these runs to the current total' (alter, function-based) versus 'the officials just declared the total is now 200' after a correction (ref-set, direct value) — both happen inside a formally reviewed transaction (dosync) with the third umpire.
(def checking (ref 1000))
(def savings (ref 500))
(defn transfer! [from to amount]
(dosync
(when (< (- @from amount) 0)
(throw (ex-info "Insufficient funds" {:from from})))
(alter from - amount)
(alter to + amount)))
(transfer! checking savings 200)
@checking ;; => 800
@savings ;; => 700
;; commute for a commutative, order-independent update
(def total-deposits (ref 0))
(dosync (commute total-deposits + 50))Transaction Retries and MVCC
When you enter a dosync block, Clojure starts a transaction that reads a consistent snapshot of every ref it touches. If, at commit time, any of those refs were changed by another transaction that committed first, Clojure discards the current attempt and silently retries the entire transaction body from the start against the fresh values. This is why the code inside a dosync block — like the code inside swap! — must be free of side effects: it may legitimately execute more than once before it finally succeeds.
Cricket analogy: Like DRS re-checking a decision from scratch if new replay angles arrive mid-review — the whole review restarts cleanly against the latest footage rather than layering partial verdicts, which is why the review process can't include irreversible actions like announcing to the crowd until it's final.
Never perform I/O or other side effects (println, HTTP calls, sending to an agent) directly inside a dosync block, since the transaction body can run more than once due to retries — any side effect could then fire multiple times. Use io! to make Clojure throw if unsafe I/O is attempted inside a transaction.
commute and Reducing Contention
For updates that are commutative and associative — like adding to a running total — alter's strict ordering is stricter than necessary and can cause needless retries under contention. commute lets the STM apply the update speculatively during the transaction but re-run it against the latest value only at commit time, without forcing the whole transaction to retry just because another commute on the same ref committed first. Use it only when the function's result genuinely doesn't depend on the order concurrent updates are applied in — using it for non-commutative logic silently introduces bugs.
Cricket analogy: Like letting two statisticians independently add boundary counts and sixes counts to a running tally in any order, since addition is commutative — order doesn't matter for the final total — but you'd never let them independently declare 'who's out', since that's not commutative and needs strict ordering.
- A dosync block groups one or more ref updates into a single atomic, consistent transaction.
- alter = function-based update (like swap!); ref-set = direct overwrite (like reset!).
- Transactions run against a consistent snapshot and retry automatically on write conflicts.
- Never put I/O or other side effects inside a dosync body — use io! to catch accidental misuse.
- commute trades strict ordering for lower contention, but only when the update function is commutative.
- Refs are the right tool whenever two or more pieces of state must change together or not at all.
Practice what you learned
1. What Clojure construct must wrap all changes to refs?
2. What concurrency control technique does Clojure's STM use internally?
3. How does alter differ from ref-set?
4. When is it safe to use commute instead of alter?
5. Why is performing I/O like println directly inside a dosync block dangerous?
Was this page helpful?
You May Also Like
Atoms in Clojure
Atoms provide Clojure's simplest managed reference type for safe, synchronous, uncoordinated mutable state backed by lock-free compare-and-swap updates.
Agents in Clojure
Agents manage a single piece of state that's updated asynchronously and independently by a queue of action functions, with built-in error handling.
Concurrency Primitives Compared
A side-by-side comparison of atoms, refs, agents, and core.async channels to help you choose the right Clojure concurrency primitive for each situation.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics