Browse Learn Clojure Foundations as a Java Developer

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.

Concurrency is about managing more than one task that can make progress during the same period. Parallelism is the narrower case where tasks literally run at the same time on different cores. Java engineers often meet both through web request handling, background jobs, scheduled work, message consumers, async I/O, and CPU-bound processing.

Clojure does not remove the JVM concurrency model. It changes where you put mutability. The default is to pass immutable values between functions and reserve changing identity for clear reference points.

Where Concurrency Pressure Comes From

Pressure Java shape Clojure design question
Many HTTP requests Thread pools call methods on shared services. Can handlers call pure functions and update explicit state only at the edge?
Background jobs Executors mutate repositories, caches, or counters. Is the job a pure transition, an atom update, an agent action, or a queue consumer?
Shared cache ConcurrentHashMap, eviction policy, locks, and callbacks. Is the cache one independent atom, an external store, or a library boundary?
Multi-step invariant Several objects must change together. Should the state be one immutable value or several refs in a dosync transaction?
User-visible responsiveness Work moves off the request thread. Should the result be a future, promise, channel, or explicit async API?

The useful question is not “does this use threads?” The useful question is “which identity changes over time, and what must stay consistent when it changes?”

The Java Problem Clojure Is Reacting Against

This Java counter looks harmless, but count++ is a read-modify-write sequence. Without synchronization or an atomic type, two threads can read the same old value and both write the same new value.

 1public final class Counter {
 2    private int count = 0;
 3
 4    public void increment() {
 5        count++;
 6    }
 7
 8    public int getCount() {
 9        return count;
10    }
11}

Java has correct fixes: synchronized, AtomicInteger, LongAdder, locks, concurrent collections, and higher-level libraries. Clojure’s lesson is not that Java tools are bad. The lesson is that mutability should be visible in the design instead of scattered across object fields.

The Clojure Counter Shape

An atom is a reference to one changing value. swap! applies a function to the current value and installs the result atomically.

 1(def counter (atom 0))
 2
 3(defn increment-counter! []
 4  (swap! counter inc))
 5
 6(let [workers (doall
 7               (repeatedly 2
 8                 #(future
 9                    (dotimes [_ 1000]
10                      (increment-counter!)))))]
11  (doseq [worker workers]
12    @worker)
13  @counter)
14;; => 2000

The important detail is that the update function passed to swap! should be pure. It may run more than once if another thread wins the compare-and-swap race first. That is different from a Java synchronized method where the body normally runs once while holding the monitor.

What Changes for a Java Engineer

Java instinct Clojure adjustment
Put mutable fields inside domain objects. Keep domain data as immutable maps or records and update values with pure functions.
Hide synchronization inside methods. Make changing identity explicit with atom, ref, agent, channel, or external system.
Treat thread safety as an implementation detail. Treat the state model as part of the design contract.
Add a lock around a failing race. Ask whether the state can be one immutable value, one atom, or one transaction.

Clojure still runs on the JVM, so thread pools, blocking calls, memory visibility, and library thread-safety still matter. The difference is that most business logic can be written as ordinary value transformation before any concurrent reference is involved.

Review Checklist

  • Identify the values that can be shared freely because they never mutate.
  • Identify the identities that actually change over time.
  • Keep state transition functions pure whenever they may be retried.
  • Use an atom only for independent state.
  • Use a coordinated model when multiple facts must change together.
  • Keep blocking I/O and external side effects at the edge of the concurrent design.

Knowledge Check

### What is the most important Clojure design question when concurrency enters a program? - [x] Which identity changes over time, and what must stay consistent when it changes? - [ ] Which class should own every thread? - [ ] How can all state be made global? - [ ] How can every function avoid the JVM? > **Explanation:** Clojure does not avoid JVM concurrency. It asks you to separate immutable values from explicit changing identities so the coordination rule is visible. ### Why should the function passed to `swap!` usually be pure? - [x] It may be retried if another thread updates the atom first. - [ ] It runs only at compile time. - [ ] It is executed by the garbage collector. - [ ] It must always return `nil`. > **Explanation:** `swap!` uses an atomic retry loop. A function with I/O or other side effects could perform that effect more than once. ### Which Java habit translates poorly to idiomatic Clojure? - [x] Spreading mutable fields across many domain objects and hiding synchronization inside methods. - [ ] Using the JVM. - [ ] Writing tests for concurrent behavior. - [ ] Measuring performance under realistic load. > **Explanation:** Clojure favors immutable domain values and explicit state references, not invisible mutation scattered through object graphs.
Revised on Saturday, May 23, 2026