Browse Learn Clojure Foundations as a Java Developer

State Management and Concurrency

Manage JVM state deliberately with immutable values, atoms, refs, agents, vars, futures, and channels while avoiding the hidden coupling Java developers often fight with locks.

Immutability does not mean “no state.” It means values do not change in place, and the few places that represent changing identity are explicit. Clojure gives Java engineers a smaller set of state tools than a typical mix of synchronized blocks, concurrent collections, executor services, and ad hoc volatile fields.

The central habit is to separate a pure transition from the reference that stores the latest value. Instead of hiding mutation inside many objects, you make updates visible with tools such as atoms for independent state, refs for coordinated state, agents for asynchronous serialized updates, and channels when message flow is the better model.

Java habit Clojure question to ask
Protect a field with a lock Can the value be immutable and replaced with swap!?
Update several objects under one lock Are these refs that should change inside one dosync transaction?
Submit background work to an executor Is this an independent future, an agent action, or a channel pipeline?
Use thread-local state Is a dynamic Var appropriate, or should the value be passed explicitly?

By the end, you should be able to look at a JVM concurrency problem and choose the smallest coordination mechanism that preserves correctness without recreating Java’s hidden shared-mutable-state problems.

In this section

  • Why Concurrency Is Hard on the JVM
    Understand the concurrency failures Java engineers recognize, including races, stale reads, lock contention, deadlocks, and broken invariants, before replacing hidden mutation with Clojure's explicit state model.
    • Understand JVM Concurrency Pressure
      Learn why JVM applications need concurrency, where Java-style shared mutable state becomes fragile, and how Clojure shifts the design toward immutable values plus explicit state references.
    • 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.
    • Translate Java Concurrency Mechanisms
      Compare Java threads, locks, volatile fields, atomics, concurrent collections, and executor services with the Clojure state and coordination tools you should reach for first.
  • Atoms, Refs, Agents, and Vars
    Choose the right Clojure reference type for changing identity: atoms for independent synchronous state, refs for coordinated transactions, agents for asynchronous updates, and Vars for namespace or dynamic context.
    • Choose Clojure State Tools
      Compare atoms, refs, agents, and Vars from a Java engineer's perspective so each changing identity has the right coordination model instead of an accidental lock or global variable.
    • Use Atoms for Independent State
      Use Clojure atoms when one in-process identity must change synchronously, and keep `swap!` update functions pure because they may be retried under contention.
    • Coordinate State with Refs and STM
      Use refs and software transactional memory when several in-process identities must change together, and keep transactions pure because Clojure may retry them.
    • Use Agents for Asynchronous State
      Use agents when one independent Clojure state value should receive serialized asynchronous updates, and choose `send` or `send-off` based on CPU-bound versus blocking work.
    • Understand Vars and Dynamic Bindings
      Understand Clojure Vars as namespace bindings, REPL-redefinable roots, and carefully scoped dynamic context rather than ordinary mutable Java variables.
  • Managing Atom State
    Use atoms for one independent, in-process identity: model values immutably, update with pure functions, and keep coordination or durability outside the atom.
    • Create and Read Atoms
      Create Clojure atoms for independent in-process state, read them safely with deref, and design the stored value as immutable data rather than a mutable Java object.
    • Update Atoms with swap! and reset!
      Choose swap! when an atom update depends on the current value, use reset! only for direct replacement, and keep swap! functions side-effect free because Clojure may retry them.
    • Design Good Atom Use Cases
      Decide when an atom is the right state boundary for counters, snapshots, caches, and REPL-managed system maps, and when refs, agents, queues, or databases are better.
  • Coordinating State with Ref Transactions
    Use refs and STM when several in-process identities must change together, and keep each dosync transaction small, pure, and focused on one invariant.
    • Understand Software Transactional Memory
      Understand Clojure software transactional memory as an optimistic in-process transaction model for refs, not as a lock replacement for every kind of state.
    • Write Ref Transactions with dosync
      Use dosync, alter, and ref-set to express coordinated ref updates, and design each transaction around one invariant that must commit atomically.
    • Avoid STM Conflicts and Retry Bugs
      Reduce STM conflicts by keeping dosync blocks short, choosing ref boundaries carefully, and removing side effects that would be unsafe if a transaction retries.
    • Choose Practical Ref Use Cases
      Use refs for practical in-process coordination problems such as inventory reservation, paired indexes, and workflow snapshots, while leaving durability and distributed truth to external systems.
  • Running Asynchronous State Tasks with Agents
    Use agents when one in-process state value should receive serialized asynchronous actions, and choose send or send-off based on whether the work is CPU-bound or may block.
    • Create Agents and Send Actions
      Create Clojure agents for one independent asynchronous state value, send action functions with send or send-off, and keep each action focused on producing the next state.
    • Read Agent State Without Lying to Yourself
      Read an agent as a completed-state snapshot, use await only when blocking is intentional, and avoid designs that need immediate results from asynchronous actions.
    • Handle Agent Failures Explicitly
      Treat agent errors as operational state: inspect failures with agent-error, decide whether to restart, and avoid hiding exceptions inside asynchronous state updates.
    • Choose Practical Agent Use Cases
      Use agents for practical asynchronous state ownership such as telemetry aggregation or ordered local side-effect coordination, while choosing queues, executors, or databases for broader task systems.
  • Compare Java Concurrency with Clojure State Tools
    Learn how Java locking, memory visibility, concurrent collections, and coordination habits map to Clojure's immutable values, atoms, refs, agents, and JVM interop boundaries.
  • Practice Clojure Concurrency with JVM Examples
    Work through practical Clojure concurrency examples that show Java engineers when to use atoms, refs, agents, queues, futures, and immutable snapshots in ordinary JVM applications.
  • Handle Side Effects in Concurrent Clojure
    Learn how Java engineers can keep I/O, logging, database writes, and other effects outside retryable Clojure state updates while preserving concurrency safety and testability.
  • Tune Clojure Concurrency Performance
    Learn how to reduce contention, keep state updates small, benchmark real concurrent workloads, and compare Clojure primitives with Java threading tools without guessing.
    • Evaluate Clojure Concurrency Overhead
      Learn where Clojure concurrency primitives spend time, including atom retries, STM conflicts, agent queues, blocking work, and the coordination costs Java developers should measure before tuning.
    • Tune Clojure State for Throughput
      Learn how to shape atoms, refs, agents, batches, and persistent collections so state changes stay small, explicit, and efficient under concurrent JVM load.
    • Benchmark and Profile Concurrent Clojure
      Learn a practical JVM measurement workflow for Clojure concurrency: isolate pure code, run realistic load, inspect queues and threads, and avoid misleading microbenchmarks.
    • Compare Clojure Concurrency with Java Threads
      Compare Clojure atoms, refs, agents, futures, and core.async with Java platform threads, virtual threads, executors, locks, and concurrent collections without confusing safety guarantees with raw speed.
  • Practice Clojure Concurrency on the JVM
    Work through practical Clojure concurrency exercises for Java engineers: atom updates, STM transfers, agent-backed background work, and measurement habits that prove correctness before tuning.
Revised on Saturday, May 23, 2026