Building a functional programming library in Java 21
Build jfp, a small dependency-free functional library for Java 21 — Option, Result, Validated, functors, monads, and monoids — with the laws that make each abstraction trustworthy, tested against a running government-scheme eligibility example.
Chapters
Why functional programming in Java?
Set up the jfp Gradle project and meet the problem this series solves — mutable, exception-driven eligibility code — before writing a single abstraction.
Functions and composition
Give Fn1 its andThen, compose, and identity, then state the two laws — identity and associativity — that let you refactor pipelines without changing behavior.
Pure functions and immutable data
What purity buys — substitution, testability, auditability — and how Java 21 records plus defensive copies give you immutable domain values without ceremony.
Algebraic data types
Model eligibility outcomes as a sealed sum type — Eligible, Ineligible, ManualReview — and let pattern matching make alternatives explicit at the boundary.
Option: absence as data
Build Option as a sealed Some|None type with map, flatMap, and fold — and use it where a missing land record is a valid state, not an error, never a null.
Result and Either: errors as values
Model expected failures — registry timeouts, invalid identities, missing citizens — as Result<T, E> so the error channel is typed, composable, and visible.
Functors: mapping inside a context
map is the whole idea of a functor — transform the value without touching the context — and its two laws are what make DTO-to-domain mapping safe to chain.
Applicatives and validation
When checks are independent, chain-of-flatMap is wrong — it stops at the first failure. Validated accumulates every error through map2 and a Semigroup.
Monads: sequencing dependent steps
flatMap exists because map cannot flatten — when each step returns a context like Result, monadic bind threads the value through and short-circuits on failure.
The monad laws, tested
Left identity, right identity, and associativity — the three laws that make flatMap chains safe to refactor, tested for real against Option and Result.
Kleisli composition for effectful functions
Ordinary compose stops working when every step returns Result — Kleisli composition reconnects A→Result<B,E> functions so registry chains stay flat.
Semigroups and monoids
An associative combine plus an identity element — the two smallest algebra laws — let Validated merge errors and folds combine results without order anxiety.
Folds, traversals, and sequencing
foldLeft collapses a collection, traverse turns List<Result<T,E>> into Result<List<T>,E>, and Validated.sequence gathers every document error at once.
Lazy evaluation and memoization
Wrap expensive evidence lookups in Lazy so they run at most once and only when a rule actually needs them — call-by-need semantics in 30 lines.
IO: effects at the boundary
An educational IO<T> that defers side effects until unsafeRun — the policy core stays pure while audit writes and event publication compose as values.
Reader: explicit environments
A Reader<Environment, T> is a function waiting for its config — it threads policy versions, thresholds, and clocks through computations without globals.
State: transitions without mutable globals
State<S, A> is a function S → (A, S) — a value plus a new state. Model application status transitions as composable steps instead of a mutable workflow field.
Async boundaries
Interop with CompletableFuture and virtual threads instead of building a runtime — independent registry lookups run in parallel, dependent ones stay sequential.
Java interoperability — when the JDK is already enough
Converters between jfp types and Optional, Stream, and CompletableFuture, plus the honest table of when to reach for the library versus plain Java.
Capstone: an auditable scheme decision pipeline
Wire Option, Result, Validated, Reader, State, and IO into one eligibility flow — pure policy core, typed failures, deferred effects, complete audit trail.