Transactional Memory Patterns in Clojure: Mastering Concurrency with STM

Learn the most useful STM patterns in Clojure, including coordinated updates, commutative counters, consistent reads, and transaction-safety rules.

Transactional memory pattern: A recurring way to structure coordinated state changes so several values move together atomically under STM.

The point of STM in Clojure is not “put mutable state in dosync.” It is to preserve invariants across several refs with as little transaction scope as possible. Refs are powerful when the design truly needs coordinated in-memory state. They are unnecessary ceremony when the problem is just one independently changing value.

Pattern 1: Coordinated Transfer

This is the classic STM case:

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

One invariant, one transaction, no partial state.

Pattern 2: Commutative Updates

If exact ordering does not matter for the final result, commute can reduce contention:

1(def hits (ref 0))
2
3(dosync
4  (commute hits inc))

This is appropriate for:

  • counters
  • tallies
  • aggregated totals

It is not appropriate when the logic depends on exact intermediate state or sequence-sensitive rules.

Sometimes a transaction must read one ref consistently while changing another. That is where ensure helps. It protects the read relationship so another transaction cannot invalidate the assumption before commit.

Use it when the invariant depends on a ref you are not directly altering.

The Retry Rule Explains Everything

The official refs model matters here: transactions may retry. That one fact drives most STM discipline:

  • keep transactions short
  • avoid blocking operations inside them
  • avoid external side effects
  • do not assume the body executes only once

That is why I/O does not belong in transactions. The transaction body is a speculative computation until commit succeeds.

Keep Effects Outside the Transaction

A reliable STM design often looks like this:

  1. compute and commit the coordinated in-memory change
  2. return a description of what happened
  3. perform logging, notifications, or other side effects afterward

That keeps the transaction retry-safe and makes the effect boundary easier to test.

Narrow Transactions Win

Large dosync bodies are usually a design smell. They increase:

  • contention
  • retry cost
  • hidden coupling

The right transaction scope is the smallest one that protects the invariant.

Return Facts From STM, Then Act on Them

One practical STM habit is to let the transaction return a small fact describing what changed, then perform external work afterward. That keeps retries safe while still making downstream behavior explicit.

For example, a transaction can return:

  • the transfer amount that committed
  • the account IDs affected
  • whether a threshold was crossed
  • a domain event map to publish after commit

That pattern preserves the invariant inside STM while keeping notifications, logs, and remote calls outside the retry boundary.

Common Misuse

The most frequent STM mistake is using refs for data that does not actually require coordinated updates. If there is no real cross-value invariant, the design is usually better with atoms or channels.

When Not to Use STM

Do not reach for refs when:

  • one value changes independently -> use an atom
  • updates should happen asynchronously -> use an agent
  • the main problem is message passing -> use channels

STM is for coordinated shared state, not for concurrency in general.

Practical Rule

Use STM when several values must change together to preserve an invariant. Keep the transaction short, side-effect-free, and explicit about whether updates are commutative or strict. Refs are at their best when the coordination boundary is small and obvious.

Revised on Thursday, April 23, 2026