Series overview
Part 1 of 205% complete
2026-03-30•15 min read

Why functional programming in Java?

Functional programming in Java is not about recreating Haskell inside the JVM. It is about a smaller claim: code is easier to reason about when data flows explicitly through transformations instead of hiding in mutable fields and thrown exceptions. This series builds jfp, a small dependency-free library that makes that claim concrete — Option, Result, Validated, Lazy, and a handful of algebraic types — while comparing every abstraction with what the JDK already gives you.

The running example is a government scheme engine: a service that decides whether a citizen is eligible for a benefit, given registry lookups, income limits, and evidence documents. It is a good fit because its requirements — auditability, explainable decisions, expected failure modes — are exactly the places where hidden state and invisible control flow hurt most.

The problem in one handler

This is the shape of the code the series is replacing. Anyone who has written a service in anger has written it:

EligibilityService.java
public EligibilityOutcome evaluate(EligibilityRequest request) {
CitizenFacts facts = citizenRegistry.find(request.citizenId()); // null? throws?
if (facts == null) {
return new Ineligible(request.schemeCode(), request.citizenId(),
List.of("citizen not found"));
}
LandRecord land;
try {
land = landRegistry.fetch(request.citizenId()); // timeout -> exception
} catch (RegistryTimeoutException e) {
return new ManualReview(request.schemeCode(), request.citizenId(), "registry timeout");
}
boolean eligible = facts.resident()
&& facts.annualHouseholdIncome() <= INCOME_LIMIT
&& land.areaHectares() > 0;
// ... and somewhere else, a mutable audit buffer is appended to
return eligible ? new Eligible(...) : new Ineligible(...);
}

Three problems hide here. Absence is null, failure is an exception, and both look identical to the compiler. The decision logic is entangled with I/O, so you cannot test the policy without mocking the world. And the audit trail — the thing a government system actually needs — is a side effect that happens “somewhere else.” By the end of the series each of those has an explicit, typed replacement.

What you will build

The artifact is a Java 21 library organized by what each piece does:

jfp/
├── core/ Fn1, Functions, Tuple2, Lazy
├── data/ Option, Result, Either, Validated, NonEmptyList
├── algebra/ Semigroup, Monoid, Foldable
├── effect/ IO, Reader, State
└── interop/ Optionals, Streams, Futures

You need JDK 21 and Gradle 8.x. Everything in the series is plain Java — no annotation processors, no bytecode tricks, no dependencies beyond JUnit and AssertJ for tests.

Project setup

settings.gradle.kts
rootProject.name = "jfp"
build.gradle.kts
plugins {
`java-library`
}
group = "in.o612.eng.jfp"
version = "0.1.0-SNAPSHOT"
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
repositories {
mavenCentral()
}
dependencies {
testImplementation(platform("org.junit:junit-bom:5.11.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.assertj:assertj-core:3.26.3")
}
tasks.test {
useJUnitPlatform()
}

The java-library plugin matters later if you publish: it separates api from implementation dependencies, so consumers of jfp do not inherit your test stack. gradle build on an empty project should succeed in a few seconds — that is your milestone check for this chapter.

The first type: a function worth naming

Java already has Function<T, R>. We define our own unary function anyway, for two reasons: it gives the library a consistent vocabulary (apply, andThen, compose), and later chapters hang methods on it that Function does not carry. For now it is deliberately minimal:

core/Fn1.java
package in.o612.eng.jfp.core;
@FunctionalInterface
public interface Fn1<T, R> {
R apply(T value);
}

And the domain value the pipeline will transform — a Java 21 record, immutable by construction:

scheme/EligibilityRequest.java
package in.o612.eng.jfp.scheme;
public record EligibilityRequest(
String citizenId,
String schemeCode
) {}

That is the whole foundation. The next chapter turns Fn1 into something composable — and states the two laws that make composition safe to refactor.

JavaFunctional Programming

Type to search the site.

↑↓ navigate⏎ openPowered by Pagefind