Browse Learn Clojure Foundations as a Java Developer

Avoid STM Conflicts and Retry Bugs

Reduce STM conflicts by keeping dosync blocks short, choosing ref boundaries carefully, and removing side effects that would be unsafe if a transaction retries.

STM removes explicit lock ordering, but it does not remove the need for design. A ref transaction can still retry, run too much work, or hide a weak state boundary.

The goal is not to avoid every retry. The goal is to make retries safe and keep contention low enough that the design remains understandable.

Conflict Review Table

Risk Symptom Better move
Long transaction Frequent retries under load Move calculation or I/O outside dosync.
Too many refs Transaction touches unrelated identities Split by invariant.
Too few refs One hot ref holds unrelated state Split independent identities.
Side effects in transaction Duplicate emails, logs, writes, or mutations Commit data, then perform effects after commit.
Mutable objects in refs STM commits but object internals race Store immutable Clojure values.

STM is optimistic. It is strongest when transactions are short, pure, and focused.

Read, Validate, Update

A good transaction usually has this shape:

 1(def seats (ref {:show-1 2}))
 2(def sales (ref []))
 3
 4(defn reserve-seat! [show-id customer-id]
 5  (dosync
 6    (let [available (get @seats show-id 0)]
 7      (when (pos? available)
 8        (alter seats update show-id dec)
 9        (alter sales conj {:show show-id
10                           :customer customer-id})
11        true))))

The validation and updates share the same transaction. A caller either reserves a seat and records the sale, or does neither.

Keep Work Out of the Retry Window

This transaction is risky because the external call could happen more than once:

1(dosync
2  (alter seats update :show-1 dec)
3  (send-confirmation-email! customer))

Use data to separate the decision from the side effect:

1(when (reserve-seat! :show-1 "c-42")
2  (send-confirmation-email! "c-42"))

In production, you may commit an outbox event or queue job instead of sending directly. The core rule is the same: retryable code should not perform irreversible work.

Boundary Choices

Situation Good boundary
Several fields form one logical value One immutable map in an atom may be simpler.
Several independent identities must coordinate Several refs in one transaction.
One identity updates independently Atom.
Updates are serialized asynchronously Agent or queue.
Data must be durable Database transaction or event log.

Avoid using refs just to make code look more “concurrent.” Use them when a real multi-identity invariant exists.

Knowledge Check

### What is the most important property of code inside `dosync`? - [x] It should be safe to retry. - [ ] It should perform all external I/O immediately. - [ ] It should mutate Java objects in place. - [ ] It should always create a new thread. > **Explanation:** STM may retry transaction bodies after conflicts. Retry-safe code is usually pure state transition logic. ### How can a transaction reduce conflict risk? - [x] Keep the transaction short and focused on one invariant. - [ ] Add unrelated refs to make it larger. - [ ] Log to a file inside every retryable block. - [ ] Use refs for durable storage. > **Explanation:** Short, focused transactions reduce the conflict window and are easier to review for retry safety. ### When might one atom be simpler than several refs? - [x] When several fields are really one logical value. - [ ] When separate identities must commit independently. - [ ] When asynchronous dispatch is required. - [ ] When data must survive JVM restart. > **Explanation:** If the state is one aggregate value, one atom can preserve the invariant without STM overhead.
Revised on Saturday, May 23, 2026