Series overview
Part 16 of 2080% complete
2026-04-24•15 min read

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

effect/Reader.java
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

scheme/PolicyQueries.java
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());
}
Evaluation.java
PolicyEnvironment rules = new PolicyEnvironment(300_000L, 18, "farmer-support-2026.1");
boolean ok = PolicyQueries.incomeWithinLimit(120_000).apply(rules); // true
String 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.

JavaFunctional Programming

Type to search the site.

↑↓ navigate⏎ openPowered by Pagefind