Browse Learn Clojure Foundations as a Java Developer

Use Agents for Ordered Side-Effect Work

Use Clojure agents for simple ordered in-process side-effect workflows, while recognizing when Java executors, queues, or durable infrastructure are the safer choice.

Agents can be useful for side effects, but only when the effect fits the agent model: in-process, asynchronous, ordered per agent, and not requiring durable delivery by itself. That is narrower than “background processing” and much narrower than “all I/O.”

Agent action: A function sent to an agent with send or send-off; it receives the current agent state and returns the next state.

The agent’s state is still important. Do not treat an agent as a hidden thread launcher with no ownership model.

A Simple Write-Behind Buffer

Suppose you want to collect recent audit events and occasionally flush them:

 1(def audit-buffer
 2  (agent []))
 3
 4(defn remember-audit [events event]
 5  (->> (conj events event)
 6       (take-last 500)
 7       vec))
 8
 9(defn record-audit! [event]
10  (send audit-buffer remember-audit event))

This is a good agent use case because one state owner holds an ordered buffer and callers do not need an immediate result.

Choose The Dispatch Function

Function Use when Avoid when
send Action is short and CPU-bound Action may block on slow I/O
send-off Action may block You are using it to hide unbounded work

For I/O, prefer send-off if an agent is still the right tool:

1(defn flush-events [events write-batch!]
2  (when (seq events)
3    (write-batch! events))
4  [])
5
6(defn flush-audit! [write-batch!]
7  (send-off audit-buffer flush-events write-batch!))

The function write-batch! is injected. That makes the effect explicit and testable.

Handle Failure Deliberately

By default, an agent that throws enters a failed state and stops accepting further work until the error is handled. Do not discover this only in production.

1(def audit-buffer
2  (agent []
3         :error-handler
4         (fn [_agent ex]
5           (println "audit buffer failed:" (.getMessage ex)))))

If it is acceptable to continue after a failed action, choose that explicitly:

1(def best-effort-audit
2  (agent []
3         :error-mode :continue
4         :error-handler
5         (fn [_agent ex]
6           (println "dropped audit event:" (.getMessage ex)))))

Use :continue for best-effort telemetry, not for critical domain workflows.

Agents Versus Java Infrastructure

Java engineers already know that “async” is not one requirement. Capacity, durability, retry, ordering, and shutdown all matter.

Requirement Agent fit Better fit when stronger guarantees are needed
Ordered in-memory state update Strong Agent is often enough
Bounded backpressure Weak BlockingQueue, executor, or channel
Durable delivery Not enough Database outbox or message broker
Retry with dead-letter handling Not enough Queue or workflow system
Integration with Java library callbacks Boundary use only Java executor or library-native API

Agents are good for small in-process ownership problems. They are not a substitute for operational infrastructure.

Testing Agent Effects

Tests that depend on agent completion should wait:

1(defn record-and-read! [events]
2  (doseq [event events]
3    (record-audit! event))
4  (await audit-buffer)
5  @audit-buffer)

Avoid using Thread/sleep in tests. Sleep guesses at timing; await expresses the dependency.

Knowledge Check

### When is an agent a good fit for side-effect work? - [x] When one in-process state owner can process ordered asynchronous actions - [ ] When exact-once durable delivery is required - [ ] When every request must wait for the result - [ ] When all Java queues must be removed > **Explanation:** Agents queue actions for one state value. Durable delivery and backpressure need stronger infrastructure. ### Why inject `write-batch!` into the agent action? - [x] It keeps the side-effect boundary explicit and replaceable in tests - [ ] It makes the agent synchronous - [ ] It disables retries - [ ] It prevents the agent from holding state > **Explanation:** Injecting effectful functions avoids hidden globals and lets tests supply a fake writer. ### Why is `await` better than `Thread/sleep` in a test that depends on agent work? - [x] `await` waits for submitted agent actions instead of guessing about timing - [ ] `await` makes the action durable - [ ] `Thread/sleep` cannot be called from Clojure - [ ] `await` runs the action twice > **Explanation:** `await` expresses the actual dependency. Sleeping may pass or fail depending on timing and machine load.
Revised on Saturday, May 23, 2026