Use Clojure agents for ordered background state updates, understand when they differ from Java executors and queues, and handle failures without hiding operational risk.
Agents are useful when work can happen later and each update belongs to one state owner. They are not a replacement for every Java executor, queue, scheduler, or messaging system. Their strength is ordered asynchronous state change.
Agent: A Clojure reference type that queues functions and applies them asynchronously to one state value.
For Java engineers, an agent feels a little like a single stateful actor or a serial executor attached to one value. That analogy is useful, but agents still use Clojure values and update functions.
Suppose a request handler should record audit events without blocking on the immediate state update. An agent can own a vector of recent events:
1(def audit-events
2 (agent []))
3
4(defn remember-event [events event]
5 (->> (conj events event)
6 (take-last 100)
7 vec))
8
9(defn record-audit! [event]
10 (send audit-events remember-event event))
11
12(defn recent-audit-events []
13 @audit-events)
record-audit! returns quickly after enqueueing the action. The agent applies actions later, in order, to its own state.
Agents fit a specific shape:
| Good fit | Why |
|---|---|
| Recent event buffer | Ordered updates to one state value |
| In-memory audit accumulator | Callers do not need the result immediately |
| Background refresh state | New state can replace old state later |
| Low-volume write-behind task | Work is independent enough to queue |
They are weaker fits when capacity, durability, retries, or distributed delivery are the main requirement. Use a real queue, executor, scheduler, database, or message broker when the operational contract demands it.
send Or send-offClojure provides two dispatch functions:
| Function | Intended work | Java mental model |
|---|---|---|
send |
CPU-bound or short actions | Shared fixed-size executor |
send-off |
Potentially blocking actions | Executor that can grow for blocking work |
Keep agent actions short when possible. If an action writes to a slow service or blocks on I/O, send-off is usually more appropriate than send.
1(defn refresh-cache [state fetch-latest]
2 (assoc state :latest (fetch-latest)))
3
4(def cache-state
5 (agent {:latest nil}))
6
7(defn refresh-cache! [fetch-latest]
8 (send-off cache-state refresh-cache fetch-latest))
The function fetch-latest is passed in so the boundary is explicit. The agent still owns only the resulting state.
If an action throws, an agent enters a failed state by default and stops processing later actions until the error is handled. That behavior is useful because silent background failure is worse than visible failure.
1(def audit-events
2 (agent []
3 :error-mode :continue
4 :error-handler
5 (fn [_agent ex]
6 (println "audit agent failed:" (.getMessage ex)))))
Use :continue only when dropping or skipping a failed action is acceptable. For critical work, use a queue with explicit retry and persistence instead.
Because agent work is asynchronous, tests and shutdown hooks need to wait when they depend on completed work:
1(defn record-all! [events]
2 (doseq [event events]
3 (record-audit! event))
4 (await audit-events)
5 @audit-events)
await is useful in tests, command-line scripts, and controlled shutdown paths. It should not become a routine request-path synchronization tool. If callers always need to wait for the result, the design is probably synchronous state, not an agent.
Agents are often compared with Java executors, but the ownership model is different.
| Need | Agent | Java executor or queue |
|---|---|---|
| Ordered state updates for one owner | Strong fit | Possible, but more boilerplate |
| Bounded backpressure | Weak fit | Strong fit |
| Durable delivery | Not enough | Use database, broker, or durable queue |
| Immediate result | Weak fit | Future or CompletableFuture may fit |
| Java framework integration | Boundary only | Strong fit |
The practical rule: use agents for simple in-process asynchronous state ownership. Use Java operational primitives for operational guarantees.