What Makes a Function Pure
A pure function has two properties: (1) deterministic — same input always produces the same output, (2) no side effects — it doesn't modify anything outside its scope: no globals, no database writes, no I/O, no mutations of its arguments. Pure functions are trivial to test (no setup, no mocks), safe to parallelize, and easy to reason about in isolation.
Impure — depends on external state
discount = 0.10 // global state
function calculateTotal(price, qty):
// depends on external variable — not deterministic
return price * qty * (1 - discount)
discount = 0.20 // change this → function behaves differently
Pure — all inputs explicit
function calculateTotal(price, qty, discount):
// same inputs → always same output
return price * qty * (1 - discount)
// Testing is trivial:
assert calculateTotal(10.0, 3, 0.10) == 27.0
assert calculateTotal(10.0, 3, 0.20) == 24.0
Side Effects and Mutations
Mutating an argument is a hidden side effect — the caller doesn't expect it. If a function receives a list and modifies it in place, every piece of code sharing that list is silently affected. Return new values instead of mutating. Isolate I/O (DB writes, HTTP calls, logging) at the boundaries of your system — keep the core logic pure.
Mutates argument — surprise!
function applyDiscount(items, discount):
for each item in items:
item.price = item.price * (1 - discount) // mutates in-place!
return items
cart = [{ name: "book", price: 20 }]
applyDiscount(cart, 0.1)
// cart is now mutated — caller doesn't know
Returns new data — no surprises
function applyDiscount(items, discount):
result = []
for each item in items:
newItem = copy(item)
newItem.price = item.price * (1 - discount)
append newItem to result
return result
cart = [{ name: "book", price: 20 }]
discounted = applyDiscount(cart, 0.1)
// cart unchanged, discounted is a new list
Push Side Effects to the Edge
You can't eliminate all side effects — programs must read input and produce output. The goal is to push them to the boundaries: UI layer, API handlers, repository methods. Keep the business logic layer pure. Pure core + thin impure shell is the architecture that makes codebases testable, readable, and maintainable long-term.
// ✗ Business logic entangled with I/O
function processOrder(orderId):
order = db.find(orderId) // I/O
if order.total > 1000:
order.discount = 0.05
else:
order.discount = 0
order.final = order.total * (1 - order.discount)
db.save(order) // I/O
email.send(order.user, "Confirmed") // I/O
return order.final
// ✓ Pure core — I/O isolated at the edges
function calculateDiscount(total): // pure
return 0.05 if total > 1000 else 0.0
function calculateFinal(total, discount): // pure
return total * (1 - discount)
function processOrder(orderId): // impure shell — thin and obvious
order = db.find(orderId) // I/O
discount = calculateDiscount(order.total) // pure
final = calculateFinal(order.total, discount) // pure
db.saveFinal(orderId, final) // I/O
email.send(order.user, "Confirmed") // I/O
return final
Code Challenge
This function reads a global, mutates its argument, and has a hidden side effect. Make all inputs explicit parameters and return a value without touching anything else.
Key takeaway
Pure functions are the safest units of code — predictable, testable, and honest. Push I/O to the edges, keep the core pure.