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.
| 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.
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.
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.
| 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.