Browse Clojure Foundations for Java Developers

Understanding Side Effects

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:

  • validates input
  • calculates a result
  • mutates object state
  • calls a repository
  • logs
  • sends a message

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.

What Usually Counts As A Side Effect?

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?

Pure Calculation Versus Effectful Interaction

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:

  • read the clock
  • mutate application state
  • log
  • hit a database
  • call a payment gateway

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.

Why Side Effects Complicate Design

Side effects are where engineering constraints show up.

  • Tests need fakes, stubs, or a running system.
  • Concurrency now matters because two callers may touch the same state.
  • Ordering matters because “log, save, publish” is different from “save, publish, log.”
  • Retries matter because the same operation may run more than once under failure or contention.

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.

A Familiar Java Refactor

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.

A Review Checklist For Spotting Hidden Effects

When reading a function, ask:

  1. Could I understand its result just from the arguments?
  2. Does it depend on the current time, random numbers, global state, or another system?
  3. Does it change a reference, send a message, or write anywhere?
  4. If I called it twice, would the outside world look different afterward?

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.

Knowledge Check

### Which expression is effectful because it depends on changing external context? - [x] `(java.time.Instant/now)` - [ ] `(inc 41)` - [ ] `(assoc order :status :paid)` - [ ] `(map inc [1 2 3])` > **Explanation:** The current time is not determined solely by the function arguments, so it is an interaction with the world, not just a pure calculation. ### Why is `order-total` easier to test than `charge-order!`? - [x] It depends only on input data and does not talk to external systems - [ ] It uses fewer parentheses - [ ] It runs on only one thread - [ ] It mutates values in place > **Explanation:** A pure function can be tested with plain input and output assertions. An effectful boundary usually needs collaborators or environment setup. ### What is the main design move Clojure encourages around side effects? - [x] Keep the decision logic pure, and perform effects explicitly at the boundary - [ ] Hide effects inside helper functions so they are harder to notice - [ ] Replace every effect with an atom - [ ] Avoid writing any functions that end with `!` > **Explanation:** Effects are part of real systems. The improvement is to isolate them so the core logic stays simple and trustworthy. ### If calling a function twice changes the outside world twice, what does that tell you? - [x] The function has an observable side effect - [ ] The function must be pure - [ ] The function is invalid Clojure - [ ] The function can only run in the REPL > **Explanation:** Observable changes beyond the return value are the practical signal that you have crossed an effect boundary.
Revised on Friday, April 24, 2026