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.
Atoms are easy to create, so the design risk is overuse. A good atom has a clear owner, a clear value shape, and a clear reason that one independent identity is enough.
For Java engineers, the useful question is not “could I make this thread-safe?” It is “what invariant does this state boundary protect?”
| Use case | Atom shape | Why it fits |
|---|---|---|
| Metrics snapshot | Map of counters | One aggregate value can be updated synchronously. |
| Runtime config snapshot | Map loaded from a file or service | Readers need a current immutable view. |
| Small in-memory cache | Map from key to value | The cache is process-local and failure is acceptable. |
| REPL-managed system map | Map of running components | Development workflow benefits from explicit replacement. |
| UI or tool state in one JVM | Map or vector | One owner can update state without distributed coordination. |
The common thread is process-local state. An atom does not make data durable, visible to other JVMs, or coordinated with external systems.
This cache records values after they have already been loaded. The atom update is pure; the expensive or effectful load happens outside the swap!.
1(def users (atom {}))
2
3(defn cached-user [id]
4 (get @users id))
5
6(defn remember-user! [id user]
7 (swap! users assoc id user)
8 user)
In a real service, the database or HTTP call belongs outside the atom update:
1(defn find-user! [load-user id]
2 (or (cached-user id)
3 (remember-user! id
4 (load-user id))))
This is fine for a small process-local cache. It is not a full cache system: there is no eviction policy, backpressure, stampede control, or cross-node invalidation.
| Requirement | Better fit |
|---|---|
| Several identities must commit together. | Refs, one aggregate value, or a database transaction. |
| Updates should run asynchronously. | Agent, queue, channel, future, or executor. |
| State must survive process restart. | Database, file, durable log, or external store. |
| Many JVMs must share the same truth. | External coordinated storage. |
| A high-contention hot counter dominates performance. | Benchmark first; consider striped counters or Java concurrency tools. |
Atoms are excellent for simple state boundaries. They are not a substitute for architecture.
Before adding an atom to production code, answer these questions:
| Question | Good answer |
|---|---|
| Who owns this atom? | One namespace or component owns creation and updates. |
| What value shape does it hold? | A documented immutable data shape. |
| What invariant does it protect? | One independent invariant, or one aggregate containing related facts. |
| What side effects happen during updates? | None inside swap!; effects happen before or after. |
| What happens after restart? | Either losing the state is fine or durable storage owns truth. |
If those answers are vague, the atom may be hiding a design problem.