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

TypeOne-linerGuide
Option<T>A value that may or may not be presentOption guide
Result<V, E>Either a success value or a typed domain errorResult guide
Try<V>A computation that may throw, turned into a valueTry guide
Validated<E, A>Like Result, but accumulates all errors instead of oneValidated guide
Either<L, R>A neutral disjoint union — neither side means failureEither guide
Lazy<T>A value computed at most once, cached after first accessLazy guide
Tuple2/3/4Typed heterogeneous groupings without a named classTuples guide
NonEmptyList<T>A list with at least one element, enforced at compile timeNEL guide
Accumulator<E, A>A value paired with a side-channel accumulation (log, metrics, audit trail)Accumulator guide
Checked interfacesCheckedFunction, TriFunction, QuadFunction, and moreChecked interfaces guide

Adding dmx-fun to your project

All artifacts are published to Maven Central under the codes.domix group.

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 versions
dependencies {
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 DSL
dependencies {
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.

dmx-fun module ecosystem — core library at the center surrounded by integration modules for Spring, Quarkus, Jackson, Jakarta, Micrometer, Resilience4j, HTTP, and more
ArtifactWhat it adds
codes.domix:funCore library — always required
codes.domix:fun-assertjFluent AssertJ assertions for all dmx-fun types
codes.domix:fun-jacksonJackson serializers/deserializers
codes.domix:fun-springSpring Framework converters and utilities
codes.domix:fun-spring-bootSpring Boot auto-configuration
codes.domix:fun-resilience4jResilience4j integration
codes.domix:fun-micrometerMicrometer instrumentation
codes.domix:fun-tracingMicrometer Tracing — distributed tracing spans for Try and Result
codes.domix:fun-observationMicrometer Observation — metrics and distributed tracing spans for Try and Result in one call
codes.domix:fun-jakarta-validationJakarta Validation integration — returns Validated instead of throwing ConstraintViolationException
codes.domix:fun-jakarta-jaxbJakarta JSON-B adapters for all dmx-fun types; JAXB adapters for Option, Result, Try, and Either
codes.domix:fun-httpjava.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:

  1. What it is — the problem it solves and when to reach for it
  2. Creating instances — factory methods and constructors
  3. Accessing / extracting values — safe and unsafe extraction
  4. Transformingmap, flatMap, filter, and friends
  5. Interoperability — conversion to other types
  6. Common pitfalls — the mistakes made most often
  7. 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.