Browse Learn Clojure Foundations as a Java Developer

Write Ref Transactions with dosync

Use dosync, alter, and ref-set to express coordinated ref updates, and design each transaction around one invariant that must commit atomically.

Refs can be read anywhere, but they can only be changed inside a dosync transaction. That rule is a feature: it makes coordinated state changes visible in the source code.

Start by naming the invariant. If the invariant does not span more than one identity, an atom is usually simpler.

Transfer Between Two Refs

 1(def checking (ref 500M))
 2(def savings  (ref 1000M))
 3
 4(defn transfer! [from to amt]
 5  (dosync
 6    (when (> amt @from)
 7      (throw
 8        (ex-info "low funds"
 9                 {:amount amt})))
10    (alter from - amt)
11    (alter to + amt)))

The transaction protects the invariant that money is not lost between the debit and credit. A Java implementation might need careful lock ordering across two account objects; the Clojure version makes the transaction boundary explicit.

Ref Update Operations

Operation Use it when Notes
alter The new value depends on the old value. Default choice for most business updates.
ref-set You already have the complete replacement value. Must still run inside dosync.
commute The update is safe to reorder. Use only when commutativity is easy to prove.
ensure You read a ref and need it unchanged before commit. Useful for validation dependencies.

Most code should start with alter. Do not use commute just because it sounds faster; only use it when update order cannot affect correctness.

Design the Transaction Boundary

Boundary mistake Better design
One huge transaction for unrelated work Split by invariant.
Separate atoms for facts that must agree Use one aggregate atom or refs in dosync.
I/O inside the transaction Commit data first, perform I/O after.
Mutable Java objects inside refs Store immutable values or isolate Java mutation outside STM.

The transaction should be small enough to review. A reviewer should be able to answer: what invariant is protected, which refs participate, and what work can retry?

Code Review Checklist

  • Every alter, ref-set, commute, or ensure appears inside dosync.
  • The transaction body has no irreversible side effects.
  • The same invariant cannot be expressed more simply as one immutable aggregate in an atom.
  • Exceptions inside the transaction are intentional and leave no external side effects behind.
  • The return value of the transaction is either used clearly or ignored deliberately.

Knowledge Check

### Why should a ref update run inside `dosync`? - [x] Clojure uses `dosync` as the transaction boundary for coordinated ref changes. - [ ] Refs cannot be read outside `dosync`. - [ ] `dosync` turns refs into atoms. - [ ] `dosync` persists data to disk. > **Explanation:** Refs can be read outside a transaction, but coordinated changes must happen inside `dosync`. ### Which ref update operation should most business code start with? - [x] `alter` - [ ] `commute` - [ ] `Thread/sleep` - [ ] `println` > **Explanation:** `alter` applies a function to the current ref value within the transaction. `commute` is only for reorder-safe updates. ### What should a reviewer ask about every ref transaction? - [x] Which invariant is protected, which refs participate, and what can retry? - [ ] Which syntax highlighting theme is active? - [ ] Whether the transaction could be moved to a macro for style. - [ ] Whether every value is stored in a separate ref. > **Explanation:** STM is useful when it protects a real invariant. Review should focus on correctness and retry safety.
Revised on Saturday, May 23, 2026