100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Concurrency Primitives Compared

A side-by-side comparison of atoms, refs, agents, and core.async channels to help you choose the right Clojure concurrency primitive for each situation.

State & ConcurrencyAdvanced11 min readJul 10, 2026
Analogies

Four Tools, Four Coordination Models

Clojure gives you four distinct concurrency primitives, and each one exists because it solves a different combination of two questions: does the update need to be coordinated with other state, and does the caller need to wait for it to finish? Atoms are uncoordinated and synchronous, refs are coordinated and synchronous via STM, agents are uncoordinated and asynchronous, and core.async channels step outside that grid entirely to provide ongoing communication between independent processes rather than managing a single value at all.

🏏

Cricket analogy: Like a team's score-keeping needs — a player's personal run tally updates alone (atom), the team total and required-run-rate update together consistently (ref/STM), the PA announcer's updates happen independently in the background (agent), and commentators relaying updates between the box and field form an ongoing conversation (core.async).

As a quick decision guide: single value, no coordination, synchronous — use an atom. Multiple values needing joint consistency, synchronous — use a ref (STM). Single value, no coordination, asynchronous — use an agent. Ongoing communication or pipelines between independent processes — use a core.async channel.

Choosing Between Atoms and Refs

The deciding question between an atom and a ref is always: does this update need to be kept consistent with some other piece of state at the same moment? If you're tracking a single independent counter, cache, or config map, an atom is simpler, has less overhead, and needs no dosync wrapping. The moment two or more values must move together — so that no other thread can ever observe one changed without the other — you need refs inside a dosync transaction, because atoms provide no mechanism to coordinate an update across more than one reference.

🏏

Cricket analogy: Choose an atom when tracking just one bowler's personal wicket count independently, but choose a ref when updating both the team's total runs and the required run rate together, since an inconsistent view where runs updated but the rate didn't would mislead the chasing batsmen.

Choosing Between Agents and core.async

Agents and core.async can look similar at a glance — both let you do work asynchronously — but they solve different problems. Choose an agent when you have one specific value that needs to be updated over time by a queue of background actions, with retained state, ordering guarantees, and built-in error handling. Choose core.async when the real requirement is an ongoing exchange of messages between multiple independent processes — a pipeline, a fan-out, a request/response conversation — rather than the management of one particular stateful value.

🏏

Cricket analogy: Choose an agent when you want a single value, like a running injury-report log, updated by queued background actions with retained history and built-in error handling, but choose core.async when you need an ongoing conversation between independent processes, like the field umpire and the third umpire exchanging review requests and rulings back and forth.

clojure
;; Atom: uncoordinated, synchronous
(def hits (atom 0))
(swap! hits inc)

;; Ref: coordinated, synchronous (STM)
(def acct-a (ref 100))
(def acct-b (ref 50))
(dosync (alter acct-a - 20) (alter acct-b + 20))

;; Agent: uncoordinated, asynchronous
(def logger (agent []))
(send logger conj :event-1)

;; core.async channel: coordinated communication between processes
(require '[clojure.core.async :as async :refer [chan go >! <!]])
(def pipe (chan))
(go (>! pipe :message))
(go (println "received" (<! pipe)))

Combining Primitives Safely in Real Systems

Production Clojure systems routinely use all four primitives together, each handling the concern it's best suited for: an atom for a local cache, refs for a coordinated ledger update, an agent for background logging, and core.async channels for a streaming pipeline to downstream consumers. The one rule that ties all four together safely is that dosync transaction bodies must stay pure — any side effect performed inside a transaction, including sending to an agent or putting onto a channel, can fire more than once if the STM transaction has to retry, so those calls belong after the transaction commits, not inside it.

🏏

Cricket analogy: Like a broadcast rig combining an atom for the 'overs bowled' ticker, a ref for the jointly consistent score-and-rate pair, an agent for background commentary logging, and a channel for live director-to-camera cueing — but you'd never let that live cueing block inside the scorer's atomic score-correction transaction, since a stalled channel could force a costly retry.

Mixing primitives carelessly is a common source of bugs: sending to an agent, writing to a core.async channel, or performing any other side effect from inside a dosync transaction can cause that side effect to fire more than once if the transaction retries. Keep transactions pure and push side effects to after the dosync block commits.

  • Atom = single value, sync, uncoordinated; Ref = multiple values, sync, coordinated (STM).
  • Agent = single value, async, uncoordinated; core.async = ongoing communication between processes.
  • Use refs when two or more values must change together consistently, like a funds transfer.
  • Use core.async when you need a pipeline or exchange, not just a single stateful value.
  • Real systems often combine all four primitives for different concerns within the same application.
  • The cardinal rule when combining them: keep dosync transaction bodies free of side effects.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#ClojureStudyNotes#ConcurrencyPrimitivesCompared#Concurrency#Primitives#Compared#Four#StudyNotes#SkillVeris#ExamPrep