Learn what counts as a side effect, why it complicates design, and how to spot clean effect boundaries in Clojure code.
Side effect: behavior that does more than return a value from inputs. It reads or changes something outside the function call, such as time, application state, a log, a file, a database, or another service.
That definition matters because Java developers often inherit code where one method does all of the following at once:
In Clojure, the goal is not to pretend those effects do not exist. The goal is to make them obvious so the pure part of the logic stays easy to test and reason about.
The table below is a better first-pass checklist than a purely philosophical definition.
| Code shape | Why it is effectful | Better design question |
|---|---|---|
(println "charged") |
Writes to the outside world | Should logging happen at the boundary instead of in the calculation? |
(java.time.Instant/now) |
Reads changing external context | Should the current time be passed in as data? |
(swap! session-state assoc :user/id 42) |
Changes a shared identity over time | Does this code really need shared state here? |
(jdbc/execute! ds sql) |
Changes data outside the process | Can pure logic prepare the data first, then persist it? |
(.charge gateway amount) |
Calls an external collaborator with observable effects | Can the pure core decide what to charge before the shell performs it? |
A pure function works like a dependable formula over data:
1(defn order-total [items]
2 (reduce
3 (fn [total {:keys [qty unit-price]}]
4 (+ total (* qty unit-price)))
5 0M
6 items))
Given the same items, order-total always returns the same value. It does not:
Now compare that with an effectful boundary:
1(defn charge-order! [gateway order]
2 (.charge gateway
3 (:order/id order)
4 (order-total (:order/items order))))
charge-order! is not bad code just because it is effectful. It is doing something useful in the world. The important thing is that the pricing logic lives in order-total, where it is simple to test and reuse.
Side effects are where engineering constraints show up.
That last point is easy to miss when moving from Java to Clojure. A pure calculation is cheap to retry. A database write or println is not.
Suppose a Java service method validates an order, computes its amount, writes to the database, and publishes an event. A common first Clojure rewrite is still too mixed together.
A better split is:
1(defn prepare-charge [order]
2 (let [amount (order-total (:order/items order))]
3 {:updated-order (assoc order :order/total amount :order/status :ready-to-charge)
4 :event {:event/type :order/ready-to-charge
5 :order/id (:order/id order)
6 :amount amount}}))
That function is still pure. It describes the next order value and the event to publish. A separate boundary function can persist the order and publish the event.
This is one of the core Clojure habits: let pure functions decide what should happen, then let effectful functions carry it out.
When reading a function, ask:
If the answer to any of 2, 3, or 4 is yes, you are looking at an effect boundary.
That is not automatically wrong. It is just a signal that this code deserves explicit placement and naming.