Browse Clojure Foundations for Java Developers

Managing State Changes

Use atoms, refs, and agents deliberately, while keeping state transitions pure and effect boundaries explicit.

Once you separate value from identity, state changes become easier to reason about.

The value itself stays immutable. The changing identity points to a different value over time.

That is the core Clojure model, and it leads to a better question than “How do I mutate this object safely?”

The better question is:

Which reference type should own this identity, and can the transition itself stay pure?

Choosing The Smallest Useful State Tool

You do not need a different reference type for every field. Start with the smallest tool that matches the coordination problem.

Situation Clojure tool Why it fits
One independent identity, updated synchronously atom Simple atomic updates to a single current value
Multiple in-memory identities must change together ref + dosync Coordinated transactional updates
One identity should update asynchronously agent Queued actions without blocking the caller

If the real problem is a database transaction, Kafka delivery, or coordination across processes, these tools do not magically replace that infrastructure. They manage in-process state.

Keep The Transition Pure, Even When The State Changes

An atom is often the first tool you need:

1(def seat-map
2  (atom {:show/id "evening-1"
3         :reserved-seats #{}}))
4
5(defn reserve-seat [show seat-id]
6  (update show :reserved-seats conj seat-id))
7
8(swap! seat-map reserve-seat "A-12")

The important split is:

  • reserve-seat decides the next value
  • swap! installs that value as the new state

That is cleaner than burying the business rule inside mutable object methods.

It also matters because swap! may retry under contention. The update function can run more than once, so keep it free of logging, database calls, and other effects.

Use Refs When Consistency Spans Multiple Identities

Refs are for coordinated in-memory change, not as a fancier atom.

 1(def inventory (ref {"SKU-1" 10}))
 2(def reservations (ref {}))
 3
 4(defn reserve-stock! [order-id sku qty]
 5  (dosync
 6    (let [available (get @inventory sku 0)]
 7      (when (< available qty)
 8        (throw (ex-info "Insufficient inventory"
 9                        {:order/id order-id
10                         :sku sku
11                         :requested qty
12                         :available available})))
13      (alter inventory update sku - qty)
14      (alter reservations assoc order-id {:sku sku :qty qty}))))

This is a good fit because both identities must change together. Either the reservation exists and inventory decreases, or neither change should happen.

Two cautions matter here:

  • keep dosync blocks free of I/O because transactions may retry
  • use refs for in-memory coordination, not as a replacement for database transactions across services

Use Agents For Serialized Asynchronous Updates

Sometimes the caller should not wait for a state update to finish. That is where an agent can fit.

1(def audit-log (agent []))
2
3(defn append-audit [entries event]
4  (conj entries event))
5
6(send audit-log append-audit
7      {:event/type :order/submitted
8       :order/id   42})

This is useful for background accumulation or other asynchronous updates to one identity.

Agents are not a general queueing platform, and they are not a replacement for a durable messaging system. They are a focused tool for asynchronous state transitions inside the process.

Common Java Migration Mistakes

Mistake Why it causes trouble Better move
Putting side effects inside swap! or dosync Retries can duplicate the side effect Keep the update function pure and do effects before or after the state change
Reaching for shared state too early Extra identities increase coordination and testing cost Pass values through pure functions until a real shared identity is required
Using a ref for a single independent value STM adds coordination machinery you do not need Start with an atom
Treating refs as distributed transactions Refs only coordinate in-process identities Use the right external system for cross-process consistency

If this page feels more operational than philosophical, that is intentional. State is where many Java habits either translate cleanly or become expensive.

Knowledge Check

### When is an atom usually the best fit? - [x] When one independent identity needs synchronous updates - [ ] When multiple identities must change together transactionally - [ ] When work must be durable across processes - [ ] When every update should be asynchronous by default > **Explanation:** Atoms are the default tool for one independent in-memory value that changes over time. ### Why should the function passed to `swap!` stay pure? - [x] Because `swap!` may retry the function under contention - [ ] Because atoms cannot hold maps - [ ] Because pure functions always run faster - [ ] Because `swap!` only accepts anonymous functions > **Explanation:** Retryable update logic should not contain side effects that could run more than once. ### What problem do refs solve that atoms do not? - [x] Coordinated, synchronous updates across multiple identities - [ ] Automatic persistence to disk - [ ] Communication with Java objects - [ ] Making values mutable in place > **Explanation:** Refs and STM are for cases where several related values must change together consistently. ### What is the main reason to use an agent? - [x] To queue asynchronous updates to one identity without blocking the caller - [ ] To perform distributed transactions - [ ] To make `dosync` faster - [ ] To avoid using immutable values > **Explanation:** Agents are about asynchronous state transitions, not mutable objects or durable messaging infrastructure.
Revised on Friday, April 24, 2026