Understand purity as deterministic behavior plus no observable side effects, and learn why Clojure treats it as a design goal rather than a hard rule.
A pure function is a function whose result is determined only by its arguments and that produces no observable side effects while computing that result.
For Java engineers, that definition matters because it changes how you structure logic. You stop asking, “which object owns this behavior?” and start asking, “what values does this calculation need, and what value should it return?”
In day-to-day Clojure work, a function is pure if both of these are true:
That means no:
This pure function depends only on its inputs:
1(defn line-total [{:keys [qty unit-price]}]
2 (* qty unit-price))
Given the same item map, line-total will always return the same number.
This function is not pure:
1(import '(java.time Instant))
2
3(defn stamp-order [order]
4 (assoc order :processed-at (Instant/now)))
The same input order can produce different outputs at different times, so the function is input-independent only in appearance.
Clojure does not have a pure keyword. A function becomes pure because of how it behaves.
That matters because two functions can look similar while having very different properties:
1(defn discounted-total [subtotal discount-rate]
2 (* subtotal (- 1 discount-rate)))
3
4(def tax-rate (atom 0.13M))
5
6(defn final-total [subtotal discount-rate]
7 (+ (discounted-total subtotal discount-rate)
8 (* subtotal @tax-rate)))
discounted-total is pure.
final-total is not, because it reads external state from tax-rate. If some other part of the system changes that atom, the same call can produce a different answer later.
That is the heart of the distinction:
Pure functions are referentially transparent. In practical terms, that means you can replace a call with its result without changing the meaning of the program.
Example:
1(def item {:qty 2 :unit-price 15M})
2
3(+ (line-total item)
4 (line-total item))
Because line-total is pure, you can reason about this as:
1(+ 30M 30M)
That sounds small, but it is one of the big reasons functional code becomes easier to test, refactor, and parallelize. You are reasoning about values, not about hidden interactions.
New Clojure developers sometimes hear “pure function” and assume that only toy arithmetic counts.
That is too narrow.
A function can still do serious work and remain pure:
Example:
1(defn order-subtotal [order]
2 (->> (:items order)
3 (map line-total)
4 (reduce + 0M)))
This is still pure. The function is doing meaningful application work, but it is doing it entirely from input data.
One of Clojure’s strengths is that it does not force an unrealistically pure world.
Programs still need:
Clojure’s philosophy is not “pretend these do not exist.”
It is closer to:
That is why the phrase pure core, impure shell is so useful for Java engineers moving into idiomatic Clojure.
Pure functions in Clojure are often closer to:
than to:
That does not mean “Java cannot be functional.” It means Java makes purity a discipline, while Clojure makes purity a more natural default design move.
If a function throws the same validation error for the same bad input and still does not touch external state, many engineers treat that as functionally pure enough for design purposes.
For example:
1(defn non-empty-name [s]
2 (when (clojure.string/blank? s)
3 (throw (ex-info "Name must not be blank" {:value s})))
4 s)
The behavior is still entirely determined by the input. That is very different from a function that logs, sleeps, queries a service, or reads mutable state.
When reviewing a function, ask:
If the answers are:
then you are probably looking at a pure function.