Browse Clojure Foundations for Java Developers

Concurrency Primitives

Choose atoms, refs (STM), or agents based on whether state updates must be coordinated and whether they are synchronous.

Java developers often learn concurrency through a toolbox of lower-level choices:

  • threads
  • executors
  • locks
  • atomics
  • concurrent collections

Clojure changes the starting point. You still care about throughput and correctness, but the first question becomes:

What kind of changing identity do I have, and how should updates to it be coordinated?

The core reference types are easier to choose once you separate two concerns:

  • does the state change independently or in coordination with other state?
  • should the caller wait for the change or hand it off asynchronously?

The diagram below is a useful first-pass decision rule.

    flowchart TD
	    Coord{"Do updates need coordination across multiple identities?"}
	    Coord -- "Yes" --> Ref["ref + dosync (coordinated transactional state)"]
	    Coord -- "No" --> Async{"Should the caller wait for the new state now?"}
	    Async -- "Yes" --> Atom["atom (independent synchronous updates)"]
	    Async -- "No" --> Agent["agent (independent asynchronous actions)"]

What to notice:

  • atom is the default for a single independent piece of state
  • ref is for coordinated in-memory transactions
  • agent is for queued asynchronous actions against a single identity

Atoms: Independent, Synchronous State

Use an atom when one identity changes independently and callers want the new value immediately.

1(def stats (atom {:requests 0 :errors 0}))
2
3(defn record-request [state]
4  (update state :requests inc))
5
6(swap! stats record-request)
7@stats
8;; => {:requests 1, :errors 0}

This is a good fit for:

  • counters
  • caches
  • application-local configuration
  • stateful UI or service-level accumulators

swap! uses a compare-and-set retry loop, so the update function must be safe to run more than once. Keep it pure.

Java mental model: roughly like AtomicReference<T> with an update function, but used pervasively with immutable maps and vectors.

Refs And STM: Coordinated Transactions

Use ref when multiple identities must change together or not at all.

1(def checking (ref 1000))
2(def savings (ref 2000))
3
4(defn transfer! [amount]
5  (dosync
6    (alter checking - amount)
7    (alter savings + amount)))

This is not a replacement for a database transaction. It is a way to coordinate in-memory state safely.

Refs are appropriate when:

  • the values are related
  • consistency across those values matters
  • the update should appear as one logical step

Like atom updates, STM transactions may retry. Keep side effects out of dosync.

Agents: Independent, Asynchronous Actions

Use an agent when state changes should happen asynchronously and the caller does not need the new value immediately.

1(def audit-log (agent []))
2
3(send audit-log conj {:event :login :user-id 42})

Agents are useful for:

  • background accumulation
  • serialized asynchronous updates
  • “fire and continue” workflows where the state change should not block the caller

They are not a substitute for queues, streams, or a full actor system. They are a focused tool for asynchronous state transitions around one identity.

What These Primitives Do Not Cover

New Clojure developers sometimes confuse reference types with task constructs. They are related, but not identical.

  • atom, ref, and agent manage state over time
  • future, promise, and core.async help with asynchronous work and coordination

That distinction matters. Choosing the wrong tool is often a sign that the real question is about work scheduling, not shared state.

A Selection Checklist For Java Engineers

Ask these questions in order:

  1. Is this one independent identity or several related identities?
  2. Does the caller need the update to complete now?
  3. Can the update function be pure and retry-safe?

If you answer:

  • one identity + now -> start with atom
  • several identities + one consistent transaction -> consider ref
  • one identity + later -> consider agent

That rule will get you surprisingly far in ordinary application code.

Knowledge Check: Choosing a Primitive

### Which primitive is the best fit for coordinated updates to multiple values? - [x] Refs (STM) inside `dosync` - [ ] Atoms - [ ] Agents - [ ] Keywords > **Explanation:** Refs and STM are designed for coordinated, transactional updates across multiple in-memory identities. ### Why should you avoid side effects inside `swap!` or `dosync`? - [x] Because the update can be retried, causing side effects to happen more than once. - [ ] Because side effects are forbidden in Clojure. - [ ] Because side effects are always slow. - [ ] Because refs and atoms can only store numbers. > **Explanation:** Contention can force retries. A side effect inside `swap!` or `dosync` can therefore happen more than once. ### What’s the main difference between agents and atoms? - [x] Agents update asynchronously; atoms update synchronously. - [ ] Agents can’t hold collections. - [ ] Atoms require locks; agents do not. - [ ] Agents are immutable; atoms are mutable. > **Explanation:** Both hold immutable values. The practical difference is timing: atom updates happen in the caller's flow, while agent actions are queued for asynchronous execution.
Revised on Friday, April 24, 2026