Learn when atoms fit, how swap! retries work, and how to use validators, watches, and compare-and-set! safely in concurrent Clojure code.
Atom: A synchronous reference type for one independently changing value. Atoms use compare-and-swap under the hood, so each update is atomic, but they do not coordinate with other references.
Atoms are the simplest shared-state tool in Clojure, and they are often the right one. If you have one value that changes over time, and that value does not need to move in lockstep with other values, an atom is usually the cleanest choice. Counters, caches, in-memory indexes, feature flags, and runtime configuration snapshots often fit this model well.
The key constraint is independence. An atom is excellent when one value can change on its own. It is the wrong choice when several pieces of state must remain consistent together, because atoms give you atomic updates to one reference at a time, not cross-reference transactions.
Use an atom when all of these are true:
That last point matters most. If you have :inventory and :balance that must change together, two separate atoms are a trap. If you have one cache map or one metrics snapshot that changes independently, an atom is a good fit.
1(def app-state
2 (atom {:feature-flags #{:new-search}
3 :request-count 0
4 :last-reload nil}))
5
6(swap! app-state update :request-count inc)
The workhorse operation is swap!. It reads the current value, applies your function, and attempts to install the new value atomically. If another thread wins the race first, Clojure retries your function with the newer value.
That retry behavior is why the update function passed to swap! must be free of accidental side effects. Logging, HTTP calls, database writes, and random value generation inside the update function can happen more than once under contention.
1(def counter (atom 0))
2
3(swap! counter inc)
4(swap! counter + 10)
For structured state, prefer small pure transformation functions:
1(defn mark-user-online [state user-id]
2 (assoc-in state [:users user-id :status] :online))
3
4(swap! app-state mark-user-online 42)
Use reset! only when you truly want to replace the current value without deriving it from the old one:
1(reset! app-state {:feature-flags #{}
2 :request-count 0
3 :last-reload (java.time.Instant/now)})
compare-and-set! is useful when you want an explicit optimistic update that should succeed only from one known state:
1(def lifecycle (atom :starting))
2
3(compare-and-set! lifecycle :starting :running)
4;; => true or false
Validators let you reject invalid states before they are installed:
1(def port
2 (atom 8080
3 :validator #(<= 1 % 65535)))
Watches are useful for observation and integration boundaries, but they should stay lightweight. A watch is a notification hook, not a business-logic pipeline.
1(add-watch app-state :audit
2 (fn [_ _ old-state new-state]
3 (when (not= (:feature-flags old-state)
4 (:feature-flags new-state))
5 (println "Feature flags changed"))))
The most common atom mistakes are architectural, not syntactic:
swap! functionsIf your update rule depends on several references staying consistent, move up a level to refs and STM. If your updates are naturally asynchronous and should be serialized in the background, agents are a better fit.
If you are deciding between Clojure’s main state tools, this is the shortest useful rule set:
That distinction is more important than memorizing API details. Most concurrency bugs in Clojure come from choosing the wrong coordination model, not from forgetting the name of a function.