Browse Clojure Foundations for Java Developers

Identifying Pure and Impure Functions

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.

The Fastest Review Questions

When reading a function, ask:

  1. What values come in through arguments?
  2. What values come in from somewhere else?
  3. What changes outside the function when it runs?
  4. Would the same call behave the same way tomorrow?

Those questions usually tell you what you need to know faster than any abstract definition.

A Pure Function Example

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:

  • the result depends only on lifetime-spend
  • it reads no external state
  • it writes nothing
  • it can be tested with plain values

Clear Impure Cases

These are the most common impurity sources:

  • reading the current time
  • reading random values
  • printing or logging
  • network or database access
  • reading environment variables or mutable globals
  • updating atoms, refs, agents, vars, or Java mutable objects

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.

A Mixed Function Is Often The Real Problem

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:

  • performs pure validation and transformation
  • triggers payment side effects
  • saves to a repository
  • prints a log line

That is not automatically wrong, but it is a sign you should separate concerns if the logic grows.

A better shape is often:

  • pure function for validation and decision-making
  • thin impure shell for payment, persistence, and logging

Deterministic Errors Are Not The Same As Side Effects

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.

Classification Table

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.

Beware Of “Looks Pure” Functions

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.

How To Refactor Toward Purity

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.

Knowledge Check

### What is the fastest practical way to identify impurity in a function? - [x] Ask what external state it reads and what it changes outside the call - [ ] Check whether it uses `defn` - [ ] Count the number of lines - [ ] See whether it returns a collection > **Explanation:** Hidden reads and side effects are usually what distinguish an impure function from a pure one. ### Why is `(swap! counter inc)` impure? - [x] It updates shared state outside the function's local arguments and return value - [ ] `inc` is an impure function - [ ] Atoms cannot hold numbers - [ ] `swap!` always throws exceptions > **Explanation:** The impurity comes from changing an atom, not from the arithmetic itself. ### Which function is the better candidate to extract as a pure core? - [x] The part that validates and transforms order data before payment or persistence - [ ] The part that prints logs - [ ] The part that reads the current time - [ ] The part that saves directly to the database > **Explanation:** Pure core logic is the portion that can be expressed entirely as input-to-output transformation. ### Why can a short function still be impure? - [x] Because brevity does not remove hidden dependencies such as time, config, or mutable state - [ ] Because short functions always mutate values - [ ] Because pure functions must be at least five lines long - [ ] Because all arithmetic in Clojure is impure > **Explanation:** The shape of the code matters less than what the code depends on and what it changes.
Revised on Friday, April 24, 2026