Introduction
What is dmx-fun?
dmx-fun is a functional programming toolkit for Java 25+. It fills the gap between plain Java and full FP frameworks by providing a small set of precisely scoped types that eliminate the most common sources of runtime surprises: null references, swallowed exceptions, and silent partial failures.
The library depends on JSpecify for its null-safety
annotations (@NullMarked) — see ADR-008 for the rationale.
An optional fun-jackson module adds JSON serialization for all types.
Core philosophy
Make illegal states unrepresentable.
Types like Option<T>, Result<V,E>, and NonEmptyList<T> encode constraints
directly in the type system. A method returning Option<T> cannot accidentally
return null; a method returning NonEmptyList<T> cannot return an empty list.
The compiler enforces the contract, not a runtime check.
Explicit over implicit.
Absent values, domain errors, and thrown exceptions are all surfaced as first-class
values. Nothing is hidden in a null return, a swallowed catch block, or an
unchecked exception that only surfaces in production.
Composable by design.
Every type provides map, flatMap, and conversion methods to the other types.
A Try converts to Result; a Result converts to Option; a Validated
converts to Result. Pipelines stay clean because the plumbing is built in.
Pattern matching as the primary API.
All types are sealed interfaces with record variants (Option.Some/Option.None,
Try.Success/Try.Failure, Result.Ok/Result.Err, etc.). Java 21+ exhaustive
switch expressions are the idiomatic way to branch on them — compiler-checked,
without instanceof boilerplate, and with destructuring built in.
The type landscape
| Type | One-liner | Guide |
|---|---|---|
Option<T> | A value that may or may not be present | Option guide |
Result<V, E> | Either a success value or a typed domain error | Result guide |
Try<V> | A computation that may throw, turned into a value | Try guide |
Validated<E, A> | Like Result, but accumulates all errors instead of one | Validated guide |
Either<L, R> | A neutral disjoint union — neither side means failure | Either guide |
Lazy<T> | A value computed at most once, cached after first access | Lazy guide |
Tuple2/3/4 | Typed heterogeneous groupings without a named class | Tuples guide |
NonEmptyList<T> | A list with at least one element, enforced at compile time | NEL guide |
Accumulator<E, A> | A value paired with a side-channel accumulation (log, metrics, audit trail) | Accumulator guide |
| Checked interfaces | CheckedFunction, TriFunction, QuadFunction, and more | Checked interfaces guide |
Adding dmx-fun to your project
All artifacts are published to Maven Central under the codes.domix group.
Using the BOM (recommended when using multiple modules)
fun-bom is a Bill of Materials that centralizes all dmx-fun artifact versions
in one place. Import it once and omit the version from every individual module
declaration — the BOM keeps them in sync automatically.
Gradle
// Kotlin DSL — import the BOM, then declare modules without versionsdependencies { implementation(platform("codes.domix:fun-bom:0.1.0"))
implementation("codes.domix:fun") implementation("codes.domix:fun-jackson") // version managed by BOM implementation("codes.domix:fun-spring-boot") // version managed by BOM}// Groovy DSLdependencies { implementation platform('codes.domix:fun-bom:0.1.0')
implementation 'codes.domix:fun' implementation 'codes.domix:fun-jackson' implementation 'codes.domix:fun-spring-boot'}Maven
<!-- Import the BOM in dependencyManagement --><dependencyManagement> <dependencies> <dependency> <groupId>codes.domix</groupId> <artifactId>fun-bom</artifactId> <version>0.1.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement>
<!-- Declare modules without versions --><dependencies> <dependency> <groupId>codes.domix</groupId> <artifactId>fun</artifactId> </dependency> <dependency> <groupId>codes.domix</groupId> <artifactId>fun-jackson</artifactId> </dependency> <dependency> <groupId>codes.domix</groupId> <artifactId>fun-spring-boot</artifactId> </dependency></dependencies>Without the BOM (single module)
If you only need one module, declare it directly with an explicit version. Each module guide has a copy-ready snippet in its Adding the dependency section.
Available modules
Each integration is a separate artifact with the peer dependency declared compileOnly —
the rationale is documented in ADR-022 — Integration modules as optional peer dependencies.

| Artifact | What it adds |
|---|---|
codes.domix:fun | Core library — always required |
codes.domix:fun-assertj | Fluent AssertJ assertions for all dmx-fun types |
codes.domix:fun-jackson | Jackson serializers/deserializers |
codes.domix:fun-spring | Spring Framework converters and utilities |
codes.domix:fun-spring-boot | Spring Boot auto-configuration |
codes.domix:fun-resilience4j | Resilience4j integration |
codes.domix:fun-micrometer | Micrometer instrumentation |
codes.domix:fun-tracing | Micrometer Tracing — distributed tracing spans for Try and Result |
codes.domix:fun-observation | Micrometer Observation — metrics and distributed tracing spans for Try and Result in one call |
codes.domix:fun-jakarta-validation | Jakarta Validation integration — returns Validated instead of throwing ConstraintViolationException |
codes.domix:fun-jakarta-jaxb | Jakarta JSON-B adapters for all dmx-fun types; JAXB adapters for Option, Result, Try, and Either |
codes.domix:fun-http | java.net.http.HttpClient wrapper — returns Result<T, HttpError> with typed 4xx / 5xx / timeout / network-failure variants |
How to read this guide
Each type page follows the same structure:
- What it is — the problem it solves and when to reach for it
- Creating instances — factory methods and constructors
- Accessing / extracting values — safe and unsafe extraction
- Transforming —
map,flatMap,filter, and friends - Interoperability — conversion to other types
- Common pitfalls — the mistakes made most often
- Real-world example — a concrete, idiomatic usage
Types interoperate — the Combining Types page covers the full conversion matrix and cross-type composition patterns.
For performance expectations and design trade-offs, see the Performance page.
A taste of composition
The snippet below registers a user by combining three types, each doing what
it does best: Try for exception handling, Validated for collecting all
validation errors, and Result for the typed outcome returned to the caller.
// End-to-end registration flow combining Try, Validated, and Result.//// 1. Try — wrap the HTTP body parse (may throw)// 2. Validated — accumulate ALL field validation errors// 3. Result — persist; the DB layer speaks Result<User, DbError>
public Result<User, RegistrationError> register(HttpRequest req) {
// Step 1: parse the request body — any parse exception becomes a Failure Try<RawInput> parsed = Try.of(() -> bodyParser.parse(req));
if (parsed instanceof Try.Failure<RawInput>(var ex)) { return Result.err(RegistrationError.invalidRequest(ex.getMessage())); }
RawInput input = parsed.get();
// Step 2: validate all fields — collect every error, not just the first Validated<NonEmptyList<String>, RegistrationForm> validation = validateEmail(input.email()) .combine(validatePassword(input.password()), NonEmptyList::concat, RegistrationForm::new);
// Step 3: convert and persist return switch (validation) { case Validated.Valid<?, RegistrationForm>(var form) -> userRepository.save(form) // Result<User, DbError> .mapErr(RegistrationError::dbFailure); // unify error type case Validated.Invalid<NonEmptyList<String>, ?>(var errors) -> Result.err(RegistrationError.validationFailed(errors.toList())); };}The types stay in their natural roles throughout and convert only at the boundary where the next layer demands a different type. That is the core pattern of dmx-fun: choose the right type for each concern, convert at the seam.