Reader: explicit environments
Policy code needs configuration — the income threshold, the minimum age, the rule version stamp that goes on every audit record. The usual Java answer is dependency injection at the class level or, worse, a static config holder reached from anywhere. Reader<R, A> makes the dependency a parameter of the computation: it is a function R → A wearing a wrapper that composes.
The implementation
public record Reader<R, A>(Fn1<R, A> run) {
public A apply(R environment) { return run.apply(environment); }
public <B> Reader<R, B> map(Fn1<? super A, ? extends B> mapper) { return new Reader<>(environment -> mapper.apply(run.apply(environment))); }
public <B> Reader<R, B> flatMap(Fn1<? super A, ? extends Reader<R, B>> mapper) { return new Reader<>(environment -> mapper.apply(run.apply(environment)).apply(environment)); }
public static <R, A> Reader<R, A> of(A value) { return new Reader<>(environment -> value); }
public static <R> Reader<R, R> ask() { return new Reader<>(environment -> environment); }}ask returns the environment itself — the primitive from which every other read is built by map. flatMap lets one computation choose the next read based on a previous result; note that both get the same environment.
Scheme example: policy reads
public static Reader<PolicyEnvironment, Long> incomeThreshold() { return Reader.<PolicyEnvironment>ask().map(PolicyEnvironment::incomeThreshold);}
public static Reader<PolicyEnvironment, Boolean> incomeWithinLimit(long income) { return incomeThreshold().map(limit -> income <= limit);}
public static Reader<PolicyEnvironment, String> stampedOutcome(String citizenId) { return Reader.<PolicyEnvironment>ask() .map(environment -> citizenId + " evaluated under rules " + environment.ruleVersion());}PolicyEnvironment rules = new PolicyEnvironment(300_000L, 18, "farmer-support-2026.1");
boolean ok = PolicyQueries.incomeWithinLimit(120_000).apply(rules); // trueString stamp = PolicyQueries.stampedOutcome("C-1").apply(rules);// "C-1 evaluated under rules farmer-support-2026.1"Every computation declares its environment in its type. The rule version that ends up on the audit record cannot come from a mutable global — it came through ask, which means the same call with the same environment always produces the same stamp. That is what reproducible decisions require.
Reader versus dependency injection
This is the comparison that matters in Spring Boot code, so be precise: Reader and constructor injection solve the same problem — “this computation needs config” — at different granularities. Injection binds the dependency when the object is built; Reader binds it when the computation runs. Use Reader when the same computation must run against different environments within one object graph — per-request rule versions, A/B thresholds, tenant configs. If your environment is fixed per service instance, ordinary constructor injection is simpler and already understood by everyone on the team; Reader is the tool for when the environment genuinely varies per call.