Pure Function
A pure function is a function whose output depends only on its arguments and which causes no observable side effects. Given the same inputs, it will always return exactly the same value, and nothing outside the function changes as a result of calling it. In functional programming, pure functions are the smallest unit of logic you can reason about in isolation. The more of a system's core behavior you can keep pure, the easier it becomes to explain, test, and rearrange.
▶Architecture Diagram
🔍 StructureDashed line animations indicate the flow direction of data or requests
When a function reads from global state, writes to a database, mutates a shared object, or logs to the console, its behavior depends on more than what you can see at the call site. That makes the function harder to test, because you have to construct the right external conditions before calling it. It also makes bugs harder to reproduce, because the same call can behave differently depending on what ran before it. Functions with hidden dependencies and hidden effects are the primary reason that software becomes difficult to reason about as it grows.
Functional programming borrowed the concept from mathematics, where a function is a mapping from inputs to outputs with no notion of history or mutation. Early programming languages blurred that boundary by mixing computation with I/O and state mutation in the same constructs. As software scaled and testing became more important, the value of separating the parts that compute from the parts that act became clearer. Pure functions are now a practical tool in mainstream languages, not just a theoretical construct, because they make the computing parts of a system much cheaper to verify and maintain.
A pure function has exactly two contracts: its parameters are its only inputs, and its return value is its only output. Nothing flows in through global variables, closures over mutable state, or implicit environment, and nothing flows out through mutation, logging, or I/O. Internally, the function may use intermediate values and call other pure functions, but those calls do not change anything outside their own stack frame. The result is a function that behaves like a mathematical mapping: you can reason about it in isolation, substitute calls with their results, and combine it with other pure functions without unexpected interactions.
Pure vs impure -- the same computation written two ways
// Impure: reads from external state, result changes over time
let taxRate = 0.1;
function calculateTotal(price: number): number {
return price + price * taxRate; // depends on mutable closure
}
// Pure: all inputs are explicit, output is deterministic
function calculateTotalPure(price: number, taxRate: number): number {
return price + price * taxRate;
}Making the tax rate an explicit parameter removes the hidden dependency. The pure version can be called in any order, in any environment, and always produces the same result for the same arguments.
Separating computation from action
// Pure: computes the new state without touching the database
function applyDiscount(
order: Order,
discountPercent: number
): Order {
return {
...order,
total: order.total * (1 - discountPercent / 100),
};
}
// Impure boundary: performs the action with the computed result
async function processOrder(order: Order): Promise<void> {
const discounted = applyDiscount(order, 10); // pure
await db.orders.update(discounted); // impure
}The computation lives in a pure function, the action lives at the boundary. This makes applyDiscount fully testable without a database, while processOrder stays as thin as possible.
Pure functions and impure functions are not opponents -- real programs need both. The useful distinction is about where to place them. Pure functions should own the logic: deciding what to compute, how to transform data, and what the output should be. Impure functions should own the actions: reading files, calling APIs, writing to the database, and producing output. Pushing side effects to the outer edges of the program and keeping the core computational logic pure is the architectural goal, not eliminating all impurity. A function that only reads from a database without mutating anything is still impure because its result depends on external state that can change. Purity is about predictability, not just avoiding writes.
Pure functions require making all dependencies explicit through parameters, which can make signatures grow longer when a function needs several pieces of context. Refactoring existing impure code to be pure sometimes requires threading values through several call layers before the hidden dependency is fully removed. For short scripts or one-off utilities, the overhead of strict purity can outweigh the benefits. The discipline is most valuable in code that will be tested repeatedly, composed with other logic, or maintained over time.
Pure functions are the right tool wherever computation can be separated from action -- which is most of the logic layer of a typical application. Pricing rules, validation, data transformation, sorting, filtering, and state transitions are good candidates. The payoff is that pure functions require no setup to unit test: pass arguments in, compare the return value, done. They also compose naturally, so a chain of pure transforms reads as clearly as the problem it solves. The limit is that at some point a program must interact with the world, and those interactions are inherently impure. The goal is to keep that boundary as thin and explicit as possible so the pure logic underneath it stays easy to reason about and maintain.