Mastering Clojure STM: Atomic Ref Updates with dosync, alter, and best practices
Clojure’s STM lets you manage shared mutable state safely with Refs, dosync, and alter. This guide shows how to atomically update refs, test concurrency, and avoid common pitfalls like side‑effects and long transactions.
07 Oct 2025, 01:51 UTC

Problem & Takeaway
When multiple threads need to share and mutate state, traditional locks can lead to deadlocks, priority inversion, and hard‑to‑debug race conditions. Clojure’s Software Transactional Memory (STM) offers a lock‑free alternative. By wrapping shared mutable values in Refs and updating them inside dosync blocks with alter (or commute), you guarantee atomicity, consistency, isolation, and durability (ACID) without explicit locking. The key takeaway: use Refs + dosync + alter for safe, concurrent state changes, and avoid side‑effects or long transactions inside dosync.
How STM Works
Clojure’s STM is an optimistic concurrency control system. When a thread enters a dosync block, it records the current values of all Refs it reads. On commit, the system checks whether any of those Refs have been changed by another transaction. If a conflict is detected, the transaction automatically retries until it can commit without interference. This eliminates the need for explicit locks and reduces deadlock risk.
Key components:
Ref– a reference type that holds mutable state.dosync– macro that defines a transaction boundary.alter– function that updates a Ref by applying a pure function to its current value.commute– variant ofalterthat can be reordered safely, useful for non‑conflicting updates.- Optimistic retry – automatic retry of the whole
dosyncblock if a conflict occurs.
Practical Example
Below is a step‑by‑step example that demonstrates atomic swapping of two Refs and validates the result. Run the code in a REPL (e.g., lein repl or CIDER). All commands require only read/write permissions to the current namespace.
(ns stm-demo.core
(:require [clojure.core :refer :all]))
;; Create two Refs holding integers
(def ref-a (ref 1))
(def ref-b (ref 2))
;; Atomically swap their values using dosync and alter
(dosync
(let [tmp @ref-a]
(alter ref-a (constantly @ref-b))
(alter ref-b (constantly tmp))))
;; Verify the swap
(println "ref-a:" @ref-a "ref-b:" @ref-b) ; -> ref-a: 2 ref-b: 1
Because both updates occur inside the same transaction, the swap is atomic: no other thread can see an intermediate state where one Ref is updated and the other is not.
To test concurrency, create many future threads that increment a shared counter Ref. The final value should equal the number of increments.
(def counter (ref 0))
(defn inc-counter []
(dosync
(alter counter inc)))
;; Spawn 1000 futures, each incrementing the counter
(doseq [_ (range 1000)]
(future (inc-counter)))
;; Wait a bit for all futures to finish
(Thread/sleep 2000)
(println "final counter:" @counter) ; -> final counter: 1000
Because STM automatically retries conflicting transactions, the final count is guaranteed to be correct even under high contention.
Common Pitfalls & Limits
- No side‑effects inside dosync: Printing, I/O, or network calls can be retried multiple times, leading to duplicate actions. Keep
dosyncblocks pure. - Long‑running transactions: Heavy computation inside
dosyncincreases the chance of conflicts and retries, hurting performance. Break large work into smaller transactions. - Using ref-set inside a transaction:
ref-setbypasses the transaction log and can break consistency. Preferalterorcommute. - Deadlock is still possible if you combine STM with other locking primitives. Stick to STM for shared state when possible.
- Version limits: STM is available from Clojure 1.3 onward. If you target older versions, you’ll need to upgrade.
To observe retries in action, insert a println inside a high‑contention dosync block. You will see the message printed multiple times, indicating automatic retries.
Conclusion
Clojure’s STM with Refs provides a robust, lock‑free mechanism for managing shared mutable state. By encapsulating updates in dosync and using alter (or commute), you achieve ACID semantics without the pitfalls of manual locking. Just remember: keep transactions short, pure, and avoid side‑effects. With these guidelines, you can confidently build concurrent Clojure applications that scale and remain maintainable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.