Browse Learn Clojure Foundations as a Java Developer

Avoid Shared Mutable State

Diagnose the bugs caused by shared mutable objects, then learn how Clojure uses immutable values and explicit references to make concurrent state changes easier to reason about.

Shared mutable state is data that more than one thread can read and change. Java engineers learn to protect it with synchronization, atomic classes, concurrent collections, or careful ownership rules. The hard part is that the ownership rule often lives in a comment, a convention, or one maintainer’s memory.

Clojure reduces the surface area of this problem. Most data is immutable and can be shared safely. When identity must change, the changing place is represented explicitly by an atom, ref, agent, Var, channel, database row, queue, or some other boundary.

What Goes Wrong

Problem Java example Why it hurts
Lost update Two threads read count = 41, both write 42. The program drops work without throwing an exception.
Aliasing Two services hold the same mutable List. A change in one component surprises another component.
Broken invariant Debit succeeds but credit fails. Related facts no longer agree.
Stale read One thread does not observe another thread’s write when expected. The bug appears only under timing, CPU, or deployment differences.
Lock coupling Correctness depends on acquiring several locks in the right order. Maintenance changes can introduce deadlocks.

The Clojure answer is not “never change anything.” It is “make the changing identity small, explicit, and updated by functions over immutable values.”

Replace Mutation with Value Transitions

Start by writing the rule as a pure function. This function has no global state, no lock, and no hidden mutable collection.

1(defn apply-payment [inv p]
2  (-> inv
3      (update :paid + (:amount p))
4      (update :events conj
5              {:type :paid
6               :payment (:id p)})))

That function can be tested with plain maps. It can also be used safely inside a state update because it returns a new invoice value instead of mutating the old one.

 1(def invoices
 2  (atom
 3    {"100"
 4     {:total 125M
 5      :paid 0M
 6      :events []}}))
 7
 8(defn record-payment! [id payment]
 9  (swap! invoices
10         update
11         id
12         apply-payment
13         payment))

The atom is the changing reference. The invoice map is an immutable value. That separation is the point.

Choose the Right Coordination Boundary

Need Prefer
One independent changing value Atom
Several identities must change together Ref plus dosync, or one larger immutable value
Ordered asynchronous updates to one value Agent
Work should move between stages Channel or queue
Durable shared state Database transaction or external store
Per-request context Explicit function arguments; dynamic Vars only for narrow cross-cutting cases

Do not put every piece of state in an atom. An atom is correct when the whole update can be described as a function from old value to new value for one independent identity.

Watch the swap! Boundary

Because swap! can retry, the update function must not perform irreversible work.

1;; Avoid: the email could be sent more than once if swap! retries.
2(swap! invoices
3       (fn [state]
4         (send-email! customer)
5         (update state
6                 invoice-id
7                 apply-payment
8                 payment)))

Keep side effects outside the retryable function:

1(let [result (swap! invoices
2                    update
3                    invoice-id
4                    apply-payment
5                    payment)]
6  (send-confirmation!
7    invoice-id
8    payment)
9  result)

For more complex workflows, store an event or command as data during the state transition, then have a separate effect boundary process it.

Java-to-Clojure Review Questions

  • Which objects currently mutate after construction?
  • Which mutable fields are read by more than one thread?
  • Which invariants span multiple fields, collections, or records?
  • Can the invariant be represented as one immutable value?
  • If not, does it need an atom, a ref transaction, an agent, or an external transaction?
  • Which effects must not be placed inside retryable update functions?

Knowledge Check

### Why does Clojure make immutable values the default? - [x] Immutable values can be shared across threads without another thread changing them unexpectedly. - [ ] Immutable values prevent all I/O. - [ ] Immutable values remove the need for testing. - [ ] Immutable values make every operation single-threaded. > **Explanation:** Immutability removes a major source of hidden coupling. It does not remove state, I/O, or the need to choose a coordination model. ### When is an atom a good fit? - [x] One independent identity can be updated with a pure function from old value to new value. - [ ] Several unrelated values must be changed in separate steps. - [ ] A side effect must run exactly once inside the update function. - [ ] A database transaction is required. > **Explanation:** Atoms are excellent for independent state. Coordinated multi-identity changes need a different boundary. ### What is wrong with sending an email inside a `swap!` function? - [x] The function may be retried, so the email could be sent more than once. - [ ] `swap!` cannot update maps. - [ ] Atoms only work inside `dosync`. - [ ] Email can only be sent from Java. > **Explanation:** Retryable state-transition functions should be pure. Put irreversible effects outside the retry loop or route them through an explicit effect boundary.
Revised on Saturday, May 23, 2026