Learn the most useful STM patterns in Clojure, including coordinated updates, commutative counters, consistent reads, and transaction-safety rules.
Transactional memory pattern: A recurring way to structure coordinated state changes so several values move together atomically under STM.
The point of STM in Clojure is not “put mutable state in dosync.” It is to preserve invariants across several refs with as little transaction scope as possible. Refs are powerful when the design truly needs coordinated in-memory state. They are unnecessary ceremony when the problem is just one independently changing value.
This is the classic STM case:
1(def checking (ref 1000))
2(def savings (ref 500))
3
4(defn transfer! [from to amount]
5 (dosync
6 (when (< @from amount)
7 (throw (ex-info "Insufficient funds" {:amount amount})))
8 (alter from - amount)
9 (alter to + amount)))
One invariant, one transaction, no partial state.
If exact ordering does not matter for the final result, commute can reduce contention:
1(def hits (ref 0))
2
3(dosync
4 (commute hits inc))
This is appropriate for:
It is not appropriate when the logic depends on exact intermediate state or sequence-sensitive rules.
Sometimes a transaction must read one ref consistently while changing another. That is where ensure helps. It protects the read relationship so another transaction cannot invalidate the assumption before commit.
Use it when the invariant depends on a ref you are not directly altering.
The official refs model matters here: transactions may retry. That one fact drives most STM discipline:
That is why I/O does not belong in transactions. The transaction body is a speculative computation until commit succeeds.
A reliable STM design often looks like this:
That keeps the transaction retry-safe and makes the effect boundary easier to test.
Large dosync bodies are usually a design smell. They increase:
The right transaction scope is the smallest one that protects the invariant.
One practical STM habit is to let the transaction return a small fact describing what changed, then perform external work afterward. That keeps retries safe while still making downstream behavior explicit.
For example, a transaction can return:
That pattern preserves the invariant inside STM while keeping notifications, logs, and remote calls outside the retry boundary.
The most frequent STM mistake is using refs for data that does not actually require coordinated updates. If there is no real cross-value invariant, the design is usually better with atoms or channels.
Do not reach for refs when:
STM is for coordinated shared state, not for concurrency in general.
Use STM when several values must change together to preserve an invariant. Keep the transaction short, side-effect-free, and explicit about whether updates are commutative or strict. Refs are at their best when the coordination boundary is small and obvious.