Clojure ref and STM — Coordinated State with dosync and alter | Episode 26
0views
C
CelesteAI
Description
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.
We prove it by hammering 200 concurrent random transfers across two refs. No locks, no dance — the total stays at 100.
Student code: https://github.com/GoCelesteAI/clojure-for-beginners/tree/main/episode26
Every keystroke is shown on screen with generous pauses so you can follow along at your own pace.
What You'll Learn:
- `(ref initial-value)` — a reference cell meant to coordinate with other refs
- `@ref` / `(deref ref)` — read the current value
- `(dosync body)` — open a transaction; body runs atomically (and may retry)
- `(alter ref f & args)` — update a ref inside a transaction with a pure fn
- Why `(alter r ...)` outside `dosync` throws `IllegalStateException: No transaction running` — and why that's a feature
- When to use `ref` vs `atom` — coordination vs independence
- How STM preserves invariants under concurrency without locks
Timestamps:
0:00 - Intro
0:15 - Preview: transactions in memory
0:51 - The code — two refs, a transfer! fn, an invariant
1:12 - Start the REPL, load the namespace
1:35 - @account-a and @account-b — the initial balances
1:50 - alter outside dosync — IllegalStateException (the lesson)
2:04 - (dosync (alter ...) (alter ...)) — both updates, one transaction
2:12 - Balances are updated — the transfer happened atomically
2:23 - Sum is still 100 — invariant preserved
2:33 - transfer! — one call, same guarantee
2:41 - Balances after transfer
2:58 - Control-D
3:14 - clj -M:run — 200 concurrent transfers, invariant holds
3:23 - Recap
4:01 - What's next: Episode 27
Key Takeaways:
1. A `ref` is a reference cell designed to coordinate with other refs. Use it when multiple values must stay consistent.
2. All ref updates happen inside a `dosync` transaction. Clojure enforces this — `alter` outside `dosync` throws.
3. `alter` applies a pure fn to a ref's current value; the transaction installs the result atomically.
4. If the transaction body sees inconsistent values (another transaction committed), Clojure retries it. Your fns must be pure.
5. All refs in the transaction commit together, or none do. That's the "atomic + isolated" guarantee of STM.
6. Over 200 concurrent random transfers, the total never drifts from 100. STM did the work.
Phase 5 continues. Next up: `agent` for asynchronous independent state — fire-and-forget updates that don't block the caller.
Taught by CelesteAI. Like and subscribe for more Clojure tutorials!