Skip to content

Clojure State Management Guide — Atoms, Refs, Agents, and Concurrency

DodaTech Updated 2026-06-28 2 min read

In this tutorial, you will learn about Clojure State Management Guide. We cover key concepts, practical examples, and best practices to help you master this topic.

Clojure provides four reference types for managing state -- atoms for uncoordinated synchronous updates (swap!, reset!), refs for coordinated transactional updates (alter, commute), agents for asynchronous updates (send, send-off), and vars for thread-local bindings.

Atoms

;; Atom: synchronous, uncoordinated state
(def counter (atom 0))

;; Read (dereference)
@counter                ;; 0
(deref counter)         ;; 0

;; Update
(swap! counter inc)     ;; 1
(swap! counter + 5)     ;; 6
(reset! counter 0)      ;; 0

;; Compare and swap
(compare-and-set! counter 0 100)  ;; true, now 100

Refs (STM)

;; Refs: coordinated transactional updates
(def account-a (ref 1000))
(def account-b (ref 500))

;; Transfer between accounts (transactional)
(defn transfer [from to amount]
  (dosync
    (alter from - amount)
    (alter to + amount)))

;; Ensure for prevent write skew
(defn withdraw-safe [account amount]
  (dosync
    (ensure account)
    (if (>= @account amount)
      (alter account - amount)
      (throw (Exception. "Insufficient funds")))))

Agents

;; Agent: asynchronous state updates
(def log-agent (agent []))

;; Send: asynchronous update (function runs in thread pool)
(send log-agent conj "Starting up")
(send log-agent conj "Processing...")

;; Send-off: for blocking I/O operations
(send-off log-agent (fn [log]
  (conj log "File read completed")))

;; Read
@log-agent   ;; ["Starting up" "Processing..."]

;; Await for agent completion
(await log-agent)
(await-for 5000 log-agent)  ;; timeout

Vars

;; Vars: thread-local bindings
(def ^:dynamic *db-connection* nil)

;; Thread-local binding
(binding [*db-connection* (connect-db)]
  (query-db *db-connection*))

;; The var is thread-local within binding scope
;; Different threads can have different values

;; Root binding
(def *debug* true)

;; Alter root (changes for all threads)
(alter-var-root #'*debug* not)

Watchers

;; Observe state changes
(def counter (atom 0))

;; Add watcher
(add-watch counter :printer
  (fn [key atom old-state new-state]
    (println "Changed from" old-state "to" new-state)))

(swap! counter inc)
;; "Changed from 0 to 1"

Common Mistakes

1. Using refs when atoms suffice

Refs are for coordinated changes across multiple identities. Use atoms for single-value state.

2. Blocking in agents

Use send-off for blocking operations (I/O). send uses a fixed thread pool for CPU-bound work.

3. Not handling STM retries

Transactions retry automatically. Side effects in transactions may execute multiple times. Use io! for I/O in STM.

Practice Questions

1. What is the difference between atoms and refs? Atoms are for uncoordinated single-value updates. Refs coordinate multiple values in transactions (STM).

2. How do you update an atom? (swap! atom update-fn args...) or (reset! atom new-value).

3. When should you use agents? For asynchronous updates where you don't need the result immediately, especially I/O operations.

FAQ

{{< faq question="What is STM?" >}} Software Transactional Memory. Changes to refs happen atomically, consistently, and in isolation. Conflicts retry automatically. {{< /faq >}}

{{< faq question="Can I use atoms for coordination?" >}} No, atoms don't coordinate. Use refs with dosync for coordinated changes (e.g., transferring between accounts). {{< /faq >}}

{{< faq question="What happens on agent error?" >}} The agent becomes failed. (agent-error agent) returns the error. (restart-agent agent new-state) clears it. {{< /faq >}}

What's Next

Now learn about namespaces in Clojure.

Topic Description Link
Namespaces Namespaces and requires {{< ref "09-namespaces" >}}
Protocols Protocols and records {{< ref "10-protocols" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro