<arch.design/>
Principles/Pure Functions
{ }CodeArchitecturebeginner1958functionaldeterminismside-effectstestability

Pure Functions

A pure function always produces the same output for the same input and causes no side effects — making it predictable, testable, and safe to run anywhere.

4/5
{ }
Operates at: Code level

Inside a codebase — classes, modules, files

How it works

A pure function has two properties: (1) determinism — given the same inputs, it always returns the same output; (2) no side effects — it doesn't mutate external state, write to disk, call an API, log to the console, or interact with anything outside its own scope.

The concept originates from mathematical functions and lambda calculus (Alonzo Church, 1936), and became a programming primitive with Lisp (1958). Languages like Haskell enforce purity at the type level; others leave it as a discipline.

Pure functions are the backbone of functional programming but are broadly useful in any paradigm. A codebase where business logic is composed of pure functions is dramatically easier to test (no setup, no mocks, just input/output assertions), reason about (no hidden state), parallelize (no shared mutable state), and cache (same input = same output = safe to memoize).

The practical discipline: push side effects (database writes, API calls, logging) to the edges of your system. Keep the core logic pure.

Implementation

TypeScript · Go · Rust
// ❌ Impure — depends on external state, logs as side effect
let taxRate = 0.07;
function calcTotal(price: number, qty: number): number {
  console.log("calculating..."); // side effect
  return price * qty * (1 + taxRate); // hidden external dependency
}

// ✓ Pure — deterministic, no side effects, all inputs explicit
function calcSubtotal(price: number, qty: number): number {
  return price * qty;
}

function applyTax(subtotal: number, taxRate: number): number {
  return subtotal * (1 + taxRate);
}

function formatCurrency(amount: number, symbol = "$"): string {
  return `${symbol}${amount.toFixed(2)}`;
}

// Compose pure functions — testable, parallelizable, memoizable
const subtotal = calcSubtotal(29.99, 3);    // 89.97
const total    = applyTax(subtotal, 0.07);  // 96.27
const display  = formatCurrency(total);     // "$96.27"

Why it matters

Impure functions are hard to test (require setting up external state), hard to parallelize (share mutable state), and hard to reason about (output depends on invisible global state). Pure functions eliminate all three problems.

When to use

  • Business rule calculations, transformations, validations — keep these pure
  • Data pipeline stages — each step is a pure transformation
  • Anywhere the logic needs to be unit-tested without mocks
  • Computations that benefit from memoization or caching

When NOT to use

  • I/O is inherently impure — database reads, HTTP calls, logging cannot be pure
  • Some operations require mutation for performance (in-place sort on large data)

Trade-offs

+

Trivially testable — no setup, no mocks, just call the function

Real systems need side effects — purity must be architected in, not assumed

+

Safe to parallelize — no shared mutable state

Threading purity through a framework designed around mutations (ActiveRecord, ORM) requires discipline

+

Referentially transparent — safe to memoize, cache, or inline

Performance-critical code sometimes needs in-place mutation that breaks purity

In production

Redux

Reducers must be pure functions: (state, action) => newState. This makes time-travel debugging and hot-reload possible

React

Functional components and hooks — same props = same output. React's concurrent renderer relies on this property

Apache Spark

RDD transformations (map, filter, reduce) are pure — the engine can parallelize and recover them safely

Industry adoption

4/5Widely adopted — mainstream at medium-to-large engineering orgs.

Related principles