Learn a practical review checklist for spotting hidden dependencies, side effects, and mixed functions.
Most code is not labeled “pure” or “impure.” You have to recognize it from behavior.
That is an important skill for Java engineers because a lot of migration pain comes from functions that look small but secretly depend on runtime context.
When reading a function, ask:
Those questions usually tell you what you need to know faster than any abstract definition.
1(defn loyalty-tier [lifetime-spend]
2 (cond
3 (>= lifetime-spend 10000M) :platinum
4 (>= lifetime-spend 5000M) :gold
5 (>= lifetime-spend 1000M) :silver
6 :else :standard))
Why it is pure:
lifetime-spendThese are the most common impurity sources:
Example:
1(import '(java.time Instant))
2
3(defn create-audit-entry [order-id]
4 {:order/id order-id
5 :seen-at (Instant/now)})
This function is impure because time is an external dependency.
Another example:
1(def request-count (atom 0))
2
3(defn record-request! []
4 (swap! request-count inc))
This is impure because it changes state outside the function call.
Many problematic functions are not purely pure or purely impure. They mix both kinds of work in one place.
1(defn submit-order! [order repo payment-gateway]
2 (let [validated-order (validate-order order)
3 charge-id (payment/charge! payment-gateway validated-order)
4 saved-order (repo/save! repo (assoc validated-order :charge/id charge-id))]
5 (println "Saved order" (:order/id saved-order))
6 saved-order))
This function:
That is not automatically wrong, but it is a sign you should separate concerns if the logic grows.
A better shape is often:
One subtle point matters here.
If a function throws the same exception for the same bad input, that is still very different from a function that logs, updates state, or reads the clock.
Example:
1(defn positive-qty [qty]
2 (when (neg? qty)
3 (throw (ex-info "Quantity must be non-negative" {:qty qty})))
4 qty)
This behavior is still fully determined by qty. In practice, many engineers treat that as pure enough for design and testing because the function’s behavior is still local and deterministic.
| Function Shape | Pure? | Why |
|---|---|---|
(+ x y) |
Yes | Depends only on inputs and changes nothing |
(assoc order :status :paid) |
Yes | Returns a new value without mutating the old one |
(println order) |
No | Produces an observable side effect |
(assoc order :now (Instant/now)) |
No | Reads external time |
(swap! state update :count inc) |
No | Changes shared state |
(System/getenv "PORT") |
No | Reads external process state |
That table is a good code-review shortcut.
Some functions look innocent because they are short:
1(def config (atom {:tax-rate 0.13M}))
2
3(defn total-with-tax [subtotal]
4 (+ subtotal (* subtotal (:tax-rate @config))))
This looks like a simple arithmetic function, but it is not pure because the result depends on hidden external state in config.
In code review, these are often more dangerous than obviously effectful functions because the hidden dependency is easy to miss.
When you find a mixed or hidden-context function, the first move is often to pass the needed data explicitly:
1(defn total-with-tax [subtotal tax-rate]
2 (+ subtotal (* subtotal tax-rate)))
Then the effectful boundary becomes responsible for retrieving the tax rate:
1(defn checkout-total [subtotal]
2 (let [tax-rate (:tax-rate @config)]
3 (total-with-tax subtotal tax-rate)))
That split is exactly what makes Clojure code easier to test and reason about.