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.
| 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?”
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.
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.
| 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.