Browse Learn Clojure Foundations as a Java Developer

Coordinate Multithreaded Application State with Refs

Use refs and Software Transactional Memory to coordinate related state changes in a multithreaded Clojure application without copying Java lock-ordering problems.

Many Java concurrency bugs appear when two pieces of state must change together but are protected by separate locks, updated in the wrong order, or partially changed after an exception. Clojure refs solve a narrower problem: coordinated synchronous changes to multiple state identities.

Software Transactional Memory (STM): Clojure’s transaction system for refs, where changes inside dosync commit together or retry when conflicts occur.

Use refs when the state really has separate identities and the invariant crosses those identities.

A Transfer Invariant

Consider a small account model:

1(def checking (ref {:id :checking
2                    :balance 500M}))
3
4(def savings (ref {:id :savings
5                   :balance 1000M}))

The invariant is not just “each account has a balance.” The invariant is “a transfer debits one account and credits the other as one logical change.”

 1(defn balance [account]
 2  (:balance @account))
 3
 4(defn transfer-in-tx! [from to amount]
 5  (when (< (:balance @from) amount)
 6    (throw (ex-info "Insufficient funds"
 7                    {:available (:balance @from)
 8                     :amount amount})))
 9  (alter from update :balance - amount)
10  (alter to update :balance + amount))
11
12(defn transfer! [from to amount]
13  (dosync
14    (transfer-in-tx! from to amount)))

If another transaction conflicts, the transaction can retry. That means code inside dosync must be retry-safe.

Java Locking Comparison

A Java version often starts with object monitors or explicit locks:

1public void transfer(Account from, Account to, BigDecimal amount) {
2    synchronized (from) {
3        synchronized (to) {
4            from.debit(amount);
5            to.credit(amount);
6        }
7    }
8}

That code must answer lock-order questions. What happens if another thread transfers in the opposite direction? Do all call sites acquire locks in the same order? What if logging or persistence is mixed into the critical section?

Refs move that coordination into a transaction:

Concern Java lock design Clojure ref design
Cross-account invariant Manual lock ordering One dosync transaction
Conflict behavior Blocks or deadlocks if ordered badly Retries transaction
State update shape Mutates fields Installs new immutable values
Side-effect risk Effects can occur while locks are held Effects inside dosync can repeat and should be avoided

STM does not make the domain model correct for you. It gives the update a safer execution boundary.

Simulate Concurrent Transfers

You can test the invariant under contention:

 1(defn total-balance []
 2  (+ (balance checking) (balance savings)))
 3
 4(defn reset-accounts! []
 5  (dosync
 6    (ref-set checking {:id :checking :balance 500M})
 7    (ref-set savings {:id :savings :balance 1000M})))
 8
 9(defn run-transfers! []
10  (reset-accounts!)
11  (let [start-total (total-balance)
12        tasks (doall
13                (for [_ (range 100)]
14                  (future
15                    (transfer! checking savings 1M)
16                    (transfer! savings checking 1M))))]
17    (doseq [task tasks]
18      @task)
19    {:checking (balance checking)
20     :savings (balance savings)
21     :total (total-balance)
22     :unchanged? (= start-total (total-balance))}))

The important result is not the exact final split between accounts after symmetrical transfers. The important result is that the total remains stable if every transfer commits as a whole.

Keep Effects Out Of Transactions

Database writes, log appends, emails, and remote calls should not live inside dosync. A transaction body may retry; an external effect cannot be retried safely unless the effect is explicitly idempotent.

Use this structure instead:

1(defn transfer-and-describe! [from to amount]
2  (let [event (dosync
3                (transfer-in-tx! from to amount)
4                {:type :transfer
5                 :amount amount
6                 :from (:id @from)
7                 :to (:id @to)})]
8    (publish-audit-event! event)
9    event))

The transaction computes and commits state. The audit publication happens after commit.

Choosing Atom Or Refs

Many designs do not need refs. If the entire invariant fits in one map, a single atom can be simpler:

1(def accounts
2  (atom {:checking {:balance 500M}
3         :savings {:balance 1000M}}))

Use the smallest tool that makes the invariant obvious.

Use this When
One atom One application-state value owns the whole invariant
Several refs Separate identities must change together
Agent Changes should be queued asynchronously for one state owner
Java lock A Java object or library requires lock-based access

The review question is simple: can another thread observe a half-completed domain change? If yes, the state ownership needs to change.

Knowledge Check

### When should refs be considered instead of two separate atoms? - [x] When two separate state identities must change together as one invariant - [ ] When one independent counter is updated - [ ] When work should happen asynchronously - [ ] When a value never changes > **Explanation:** Refs coordinate multiple state identities inside one transaction. Two atom updates can expose partial state between updates. ### Why should remote calls usually stay outside `dosync`? - [x] A transaction body may retry, repeating the call - [ ] Refs cannot hold maps - [ ] `dosync` only works in Java code - [ ] Remote calls automatically become asynchronous > **Explanation:** STM can retry transaction bodies under conflict. Retrying a remote call can duplicate an external effect. ### What invariant does the transfer example protect? - [x] A debit and credit commit together so the total balance stays consistent - [ ] Every transfer runs in a new operating-system process - [ ] Account maps become mutable - [ ] Reads are forbidden while transfers run > **Explanation:** The transfer is one logical state change across two refs. The transaction prevents a committed half-transfer.
Revised on Saturday, May 23, 2026