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
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:
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:
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.
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.