Series overview
Part 3 of 2015% complete
2026-04-02•15 min read

Pure functions and immutable data

A pure function has two properties: the same input always yields the same output, and evaluating it changes nothing observable outside itself — no fields written, no database touched, no network call made. That is the whole definition, and it buys you something concrete: you can evaluate a call site mentally by substituting the argument, which is why pure code is easy to test and easy to audit.

Purity in the domain

scheme/Income.java
public record Income(long annualAmount) {
public boolean exceeds(long threshold) {
return annualAmount > threshold;
}
}

exceeds is pure: it reads only its own fields. You can test it with three lines and trust it in a pipeline, in a rule engine, in a parallel stream — anywhere. Contrast it with the typical alternative: a method that reaches into a registry client or a static clock. Same signature, unbounded test surface.

Immutable domain facts

The scheme engine’s input is a snapshot of what was true about a citizen at evaluation time:

scheme/CitizenFacts.java
public record CitizenFacts(
String citizenId,
boolean resident,
long annualHouseholdIncome,
boolean verifiedAgriculturalLand
) {}

Records give you immutability of the shallow fields for free. The subtlety is mutable contents: a record holding a List<String> is only as immutable as the list. The fix is one line in the compact constructor:

scheme/ApplicationInput.java
public record ApplicationInput(
String citizenId,
int age,
long declaredIncome,
boolean resident,
List<String> documents
) {
public ApplicationInput {
documents = List.copyOf(documents);
}
}

List.copyOf defends both directions — callers cannot mutate your copy after construction, and you hand out a view that throws on mutation attempts.

Why it matters for a scheme engine

When evidence changes, you do not mutate CitizenFacts — you construct a new value. The old snapshot survives, which is exactly what an audit trail needs: “the decision at 10:14 used facts X; the corrected facts are Y.” With mutable state, the old facts are gone the moment a field is overwritten, and “what did the system know when it decided?” becomes unanswerable.

new evidence

still intact

CitizenFacts v1

landVerified = false

CitizenFacts v2

landVerified = true

audit trail

policy evaluation

new evidence

still intact

CitizenFacts v1

landVerified = false

CitizenFacts v2

landVerified = true

audit trail

policy evaluation

Production note: purity is a discipline, not a type system feature — Java will not enforce it for you. Income.exceeds could silently call System.currentTimeMillis() tomorrow and no compiler would object. The practical mitigation is cultural: keep I/O and clocks at the boundary (Parts 15–18 make that boundary a type), keep domain values as records, and let the policy core be a place where substitution always works.

JavaFunctional Programming

Type to search the site.

↑↓ navigate⏎ openPowered by Pagefind