Back to Blog

Clojure for Beginners: Clojure ref and STM — Coordinated State with dosync and alter

Celest KimCelest Kim

Video: Clojure ref and STM — Coordinated State with dosync and alter | Episode 26 by CelesteAI

Watch full page →

Clojure ref and STM — Coordinated State with dosync and alter

When two pieces of state must change together — debit one account and credit another — `atom` isn't enough. Clojure's answer is `ref` plus Software Transactional Memory. You open a transaction with `dosync`, update refs inside it with `alter`, and every change commits as a single atomic unit. If the JVM crashes mid-transaction, nothing partial lands. If another thread interleaves, the transaction restarts. The invariant (`a + b = 100`) holds forever.

Code

(ns app.core)

;; Two refs — account balances that must stay in sync.
(def account-a (ref 100))
(def account-b (ref 0))

(defn transfer!
  "Move `amount` from one ref to another, atomically. Both updates
   either happen together or not at all."
  [from to amount]
  (dosync
    (alter from - amount)
    (alter to   + amount)))

(defn total
  "Sum of both balances — the invariant."
  []
  (dosync (+ @account-a @account-b)))

(defn -main
  "Run with: clj -M:run"
  [& _]
  (println "Before       :" @account-a "/" @account-b "  total =" (total))

  (transfer! account-a account-b 30)
  (println "After  30 → B:" @account-a "/" @account-b "  total =" (total))

  (transfer! account-a account-b 20)
  (println "After  20 → B:" @account-a "/" @account-b "  total =" (total))

  ;; Hammer STM — 200 random transfers, concurrently, in both directions.
  (let [fs (doall (for [_ (range 200)]
                    (future (transfer! account-a account-b (rand-int 10))
                            (transfer! account-b account-a (rand-int 10)))))]
    (run! deref fs))

  (println "After 200 ops:" @account-a "/" @account-b "  total =" (total)))

Key Points

Watch the video above for a full walkthrough — every keystroke is shown so you can code along.

Student code: GitHub