Lazy evaluation and memoization
Some facts are expensive and conditional: a historical-benefit lookup that only one rule in the policy needs, a document OCR call that manual review might never reach. Lazy<T> wraps a supplier, defers it until get(), and memoizes the result — the functional-programming name for this is call-by-need.
The implementation
public final class Lazy<T> {
private Supplier<T> supplier; private T value; private boolean evaluated;
private Lazy(Supplier<T> supplier) { this.supplier = supplier; }
public static <T> Lazy<T> of(Supplier<T> supplier) { return new Lazy<>(Objects.requireNonNull(supplier, "supplier must not be null")); }
public synchronized T get() { if (!evaluated) { value = supplier.get(); evaluated = true; supplier = null; } return value; }
public <R> Lazy<R> map(Fn1<? super T, ? extends R> mapper) { return Lazy.of(() -> mapper.apply(get())); }
public <R> Lazy<R> flatMap(Fn1<? super T, Lazy<R>> mapper) { return Lazy.of(() -> mapper.apply(get()).get()); }}Three details matter. synchronized makes the evaluate-once guarantee hold under concurrent readers — the first caller pays the cost, everyone else gets the memoized value. supplier = null releases the captured closure (and whatever it referenced) after evaluation. And map/flatMap build new lazy computations without running anything — laziness composes.
The memoization test
@Testvoid evaluatesTheSupplierAtMostOnce() { AtomicInteger calls = new AtomicInteger(); Lazy<Integer> lazy = Lazy.of(calls::incrementAndGet);
assertThat(lazy.get()).isEqualTo(1); assertThat(lazy.get()).isEqualTo(1); assertThat(calls).hasValue(1);}
@Testvoid mapDefersEvaluationUntilGet() { AtomicInteger calls = new AtomicInteger(); Lazy<Integer> lazy = Lazy.of(calls::incrementAndGet).map(x -> x * 10);
assertThat(calls).hasValue(0); // nothing ran yet assertThat(lazy.get()).isEqualTo(10);}Scheme example: deferred evidence enrichment
Lazy<Result<IncomeRecord, RegistryError>> incomeHistory = Lazy.of(() -> incomeRegistry.load(citizenId)); // expensive historical lookup
// the policy only needs history on the manual-review path — for a clean// Eligible the registry call never happensEligibilityOutcome outcome = needsManualReview ? incomeHistory.get().fold( error -> new ManualReview(scheme.value(), citizenId, "history unavailable"), record -> evaluateWithHistory(facts, record)) // policy variant, defined by the rules : FarmerSupportPolicy.evaluate(facts, scheme);If the rule never asks, the lookup never happens — the “skip work you don’t need” optimization becomes part of the value’s type rather than an if guarding a field.
Trade-off worth stating: Lazy memoizes successfully, meaning exceptions are not cached — a throwing supplier is retried on the next get(). That is usually what you want for transient failures, but a permanently-failing lookup will be re-attempted every time. If you need to remember failure, wrap the computation in Lazy.of(() -> resultOrThrow()) so the memoized value is itself a Result.