Option<T>
Runnable example:
OptionSample.java
What is Option<T>?
Option<T> models a value that may or may not be present.
It is a sealed interface with exactly two implementations:
Some<T>— holds a non-null value.None<T>— represents absence, with no value.
Unlike a raw null, an Option is explicit in the type system:
callers are forced to handle both the present and absent cases.
Unlike java.util.Optional, Option is a sealed interface
and can be used in switch expressions with exhaustive pattern matching.
The decision to provide a custom Option<T> type instead of reusing java.util.Optional is documented in ADR-015 — Option<T> as a custom type instead of java.util.Optional.
Creating instances
Option.some(value)
Wraps a known, non-null value. Throws NullPointerException if the argument is null.
Option<String> name = Option.some("Alice");// name is Some("Alice")Option.none()
Returns the singleton absent value.
Option<String> missing = Option.none();// missing is NoneOption.ofNullable(value)
Creates an Option from a value that may be null.
Returns None when the argument is null, Some(value) otherwise.
This is the most common factory when bridging from legacy code.
String raw = fetchFromLegacyApi(); // may return nullOption<String> result = Option.ofNullable(raw);Option.fromOptional(optional)
Converts a java.util.Optional into an Option.
Optional<User> jdkOptional = repo.findById(id);Option<User> dmxOption = Option.fromOptional(jdkOptional);Converting from Result or Try
Result and Try expose toOption() to discard the error/exception track.
The static factory forms are symmetric: Option.fromResult(result) and Option.fromTry(t).
Option.fromTryOptional(t) flattens a Try<Optional<T>> in one step.
// Result -> Option: discard the error trackOption<User> fromResult1 = userResult.toOption(); // instance methodOption<User> fromResult2 = Option.fromResult(userResult); // static factory (same semantics)
// Try -> Option: discard the exception track// Note: Try.Success(null) produced by Try.run() maps to NoneOption<Config> fromTry1 = tryReadConfig.toOption(); // instance methodOption<Config> fromTry2 = Option.fromTry(tryReadConfig); // static factory (same semantics)
// Try<Optional<T>> -> Option<T>: flatten both layers in one step// Success(Optional.of(v)) -> Some(v)// Success(Optional.empty()) -> None// Failure(ex) -> None (exception discarded)Option<Item> item = Option.fromTryOptional( Try.of(() -> repository.findById(id)));Checking state
Prefer structural operations (fold, map, pattern matching)
over imperative state checks, but the predicates are available:
option.isDefined() // true if Someoption.isEmpty() // true if NoneYou can also use Java pattern matching exhaustively:
switch (option) { case Option.Some<String>(var v) -> System.out.println("got: " + v); case Option.None<String> _ -> System.out.println("nothing here");}Extracting values
| Method | When None | Notes |
|---|---|---|
get() | Throws NoSuchElementException | Only safe after an isDefined() check; prefer alternatives. |
getOrElse(fallback) | Returns fallback | Fallback is always evaluated — use getOrElseGet for expensive defaults. |
getOrElseGet(supplier) | Calls supplier | Lazily computed fallback. |
getOrNull() | Returns null | Bridge to null-expecting APIs. Avoid propagating the null further. |
getOrThrow(supplier) | Throws supplied exception | Use when absence is a programming error at that call site. |
fold(onNone, onSome) | Calls onNone | The most expressive extractor: forces you to handle both branches. |
// Preferred: fold makes both branches explicitString display = option.fold( () -> "anonymous", name -> "Hello, " + name);
// Lazy fallback — DB call only when NoneUser user = maybeUser.getOrElseGet(() -> userRepo.defaultUser());
// Throw a domain exception when absence is a bugConfig cfg = maybeConfig.getOrThrow( () -> new IllegalStateException("Config not loaded"));Transforming values
map(mapper)
Applies a function to the value if present. Returns None unchanged.
The mapper must not be null and must not return null — both throw NullPointerException eagerly,
consistent with the @NullMarked contract and the behavior of flatMap.
Option<String> upper = Option.some("hello").map(String::toUpperCase);// Some("HELLO")
Option<String> none = Option.<String>none().map(String::toUpperCase);// NoneflatMap(mapper)
Like map, but the mapper itself returns an Option.
Use this to chain operations that may also produce absence.
Option<Address> address = findUser(id) // Option<User> .flatMap(user -> user.getPrimaryAddress()); // returns Option<Address>filter(predicate)
Keeps the value only if it satisfies the predicate; otherwise returns None.
Option<Integer> positive = Option.some(-5).filter(n -> n > 0);// None — value fails the predicatepeek(action)
Runs a side-effecting action on the value (e.g., logging) without altering the Option.
No-op on None. Returns this for chaining.
Option<Order> order = findOrder(id) .peek(o -> log.debug("Processing order {}", o.id())) .filter(Order::isActive);match(onNone, onSome)
Terminal side-effecting variant of fold. Both branches return void.
option.match( () -> metrics.increment("cache.miss"), value -> metrics.increment("cache.hit"));Composing Options
Fallback chains with orElse
Return an alternative Option when this one is None.
The supplier overload is lazy — use it when computing the alternative is expensive.
// Eager alternativeOption<String> result = primary.orElse(secondary);
// Lazy alternative (computed only when primary is None)Option<String> result = primary.orElse(() -> computeSecondary());Zipping two or more Options
Combine multiple independent Option values.
Returns None if any input is None.
// Zip to a Tuple2Option<Tuple2<String, Integer>> pair = Option.zip(nameOption, ageOption);
// Zip with a custom combinerOption<Greeting> greeting = Option.zip(nameOption, localeOption, Greeting::new);Pairing a value with a derived Option — zipWith / flatZip
zipWith(mapper) applies a function to the current value and pairs the original
value with whatever the function returns. If the mapper returns None, the whole
result is None. Unlike zip(Option), the second Option is computed lazily
from the first value.
flatZip is an alias that may read more naturally at the call site.
// zipWith(Function) — pair the current value with a derived OptionOption<String> name = Option.some("alice");
Option<Tuple2<String, Integer>> result = name.zipWith(n -> lookupAge(n));// Some(Tuple2("alice", 30)) — if lookupAge returns Some(30)// None — if lookupAge returns None or name is None
// flatZip is an alias with the same semanticsOption<Tuple2<String, Integer>> same = name.flatZip(n -> lookupAge(n));
// Contrast with zip(Option) — the second Option is pre-evaluatedOption<Integer> age = Option.some(30);Option<Tuple2<String, Integer>> fromZip = name.zip(age);// Both are None when name is None, but zip always evaluates age regardlessCollecting a list with sequence
Converts a List<Option<T>> into an Option<List<T>>.
Returns None as soon as any element is None. The stream-based overload uses
Stream.gather(Gatherer.ofSequential(...)) for true short-circuit (see ADR-010).
List<Option<User>> lookups = ids.stream() .map(userRepo::findById) .toList();
Option<List<User>> allOrNone = Option.sequence(lookups);Collecting a stream with sequenceCollector
Option.sequenceCollector() is the stream Collector counterpart to sequence.
It reduces a Stream<Option<V>> to a single Option<List<V>> — Some(list) only if
every element is Some, None if any element is None.
// All Some → Option containing all valuesOption<List<String>> result = Stream.of( Option.some("alice"), Option.some("bob")) .collect(Option.sequenceCollector());// Some(["alice", "bob"])
// Any None → NoneOption<List<String>> missing = Stream.of( Option.some("alice"), Option.<String>none()) .collect(Option.sequenceCollector());// None
// Empty stream → Some([])Option<List<String>> empty = Stream.<Option<String>>empty() .collect(Option.sequenceCollector());// Some([])
// Typical use: look up all IDs, fail if any is missingOption<List<User>> allFound = ids.stream() .map(userRepo::findById) // Stream<Option<User>> .collect(Option.sequenceCollector()); // Option<List<User>>Collector facade: Options
Options is a single discoverable entry point for all Stream<Option<T>> collectors —
analogous to java.util.stream.Collectors. Import dmx.fun.Options instead of
remembering which collector lives on which type.
The decision to provide companion facade classes as pure-delegation entry points is documented in ADR-014 — Facade collectors (Results, Options, Tries) as a single entry point.
| Method | Returns | Semantics |
|---|---|---|
Options.presentToList() | List<T> | Drops None — returns only present values |
Options.sequence() | Option<List<T>> | All-or-nothing — None if any element is None |
Options.toNonEmptyList() | Option<NonEmptyList<T>> | Wraps stream into NonEmptyList, or None if empty |
// ── Options.presentToList() — keep only present values ───────────────────────List<String> names = Stream.<Option<String>>of( Option.some("alice"), Option.none(), Option.some("bob"), Option.none()) .collect(Options.presentToList());// ["alice", "bob"]
// Real-world: resolve a batch of user IDs; ignore the ones not foundList<User> found = ids.stream() .map(userRepo::findById) // Stream<Option<User>> .collect(Options.presentToList()); // List<User> — absent entries dropped
// ── Options.sequence() — all-or-nothing: None if any is None ─────────────────Option<List<User>> allOrNone = ids.stream() .map(userRepo::findById) .collect(Options.sequence());// Some([...]) only when every ID resolves; None otherwise
// ── Options.toNonEmptyList() — wrap a stream into a NonEmptyList if non-empty ─Option<NonEmptyList<String>> nel = items.stream() .filter(item -> item.isActive()) .collect(Options.toNonEmptyList());
nel.match( () -> log.warn("no active items"), items -> process(items) // items is a guaranteed NonEmptyList);Stream integration with stream()
Turns an Option into a one-element or empty stream.
Useful for integrating into Stream pipelines without filter + map.
long count = options.stream() .flatMap(Option::stream) // flatten Nones out .count();Interoperability
| Conversion | Method | Notes |
|---|---|---|
Option → Optional | option.toOptional() | Some(v) → Optional.of(v), None → Optional.empty() |
Option → Result | option.toResult(errorIfNone) | None becomes Err(errorIfNone) |
Option → Try | option.toTry(exceptionSupplier) | None becomes Failure; supplier called lazily |
Option → Either | option.toEither(leftIfNone) | Some(v) → Right(v), None → Left(leftIfNone) |
Option → CompletableFuture | option.toFuture() | Some(v) → completed future, None → failed with NoSuchElementException |
Optional → Option | Option.fromOptional(optional) | |
Result → Option | Option.fromResult(result) or result.toOption() | Err is discarded |
Try → Option | Option.fromTry(t) or t.toOption() | Failure is discarded; Success(null) → None |
Try<Optional<T>> → Option | Option.fromTryOptional(t) | Flattens both layers; Failure and Optional.empty() → None |
Either → Option | Option.fromEither(either) or either.toOption() | Left is discarded; Right(v) → Some(v) |
Option<User> user = findUser(id);
// ── Option → other types ────────────────────────────────────────────────────
// → Optional (bridge to JDK APIs)Optional<User> optional = user.toOptional();
// → Result (None becomes Err with the supplied error value)Result<User, String> result = user.toResult("User not found: " + id);
// → Try (None becomes Failure; supplier is called lazily)Try<User> tried = user.toTry(() -> new NoSuchElementException("User " + id + " not found"));
// → Either (Some → Right, None → Left with the supplied left value)Either<String, User> either = user.toEither("not-found");
// → CompletableFuture (Some → completed, None → failed with NoSuchElementException)CompletableFuture<User> future = user.toFuture();
// ── Other types → Option ────────────────────────────────────────────────────
// Optional → OptionOption<User> fromOptional = Option.fromOptional(legacyOptional);
// Result → Option (Err is discarded)Option<User> fromResult = Option.fromResult(userResult);
// Try → Option (Failure is discarded; Try.Success(null) from Try.run() → None)Option<Config> fromTry = Option.fromTry(tryReadConfig);
// Try<Optional<T>> → Option<T> (flattens both layers in one step)Option<Item> fromTryOpt = Option.fromTryOptional( Try.of(() -> repository.findById(id)));
// Either → Option (Left is discarded; Right(v) → Some(v))Option<User> fromEither = Option.fromEither(eitherValue);See the Combining Types page for the full conversion matrix and composition patterns.
Option vs null vs Optional
null | Optional<T> | Option<T> | |
|---|---|---|---|
| Null-safety | None — NPE at runtime | Good — explicit | Good — explicit |
| Exhaustive handling | No | No (runtime only) | Yes — sealed, switch patterns |
| Use in records / generics | Yes (fragile) | Discouraged (not serializable) | Yes — record-based, serializable |
| Compose with Result / Try / Either | No | No | Yes — first-class conversions |
| Holds null values | Yes | No | No |
Common pitfalls
Nested Options
Option<Option<T>> almost always indicates a mistake.
Use flatMap instead of map when the mapper itself returns an Option.
// Bad: creates Option<Option<Address>>Option<Option<Address>> nested = user.map(User::getAddress);
// Good: flat resultOption<Address> address = user.flatMap(User::getAddress);Mapper returning null
Under @NullMarked, map and flatMap both throw NullPointerException when the
mapper returns null. Use Option.ofNullable(mapper.apply(value)) explicitly if
the mapper may return null and you want None instead of an exception.
// map and flatMap both throw NPE when the mapper returns null (@NullMarked contract)Option<String> bad = option.map(User::getName); // NPE if getName() returns null
// If the mapped method may return null, convert explicitly:Option<String> safe = Option.ofNullable(option.getOrNull()) .flatMap(u -> Option.ofNullable(u.getName())); // None when getName() is nullCalling get() unconditionally
get() throws NoSuchElementException on None.
Use getOrElse, fold, or pattern matching instead.
Option as a method parameter
Prefer overloaded methods or a nullable parameter over an Option-typed parameter.
Option is designed for return types.
Real-world example
A typical service layer pattern: look up a user, find their primary address, format it, and fall back to a default — all without a single null check or try/catch.
public String resolveShippingLabel(long userId) { return userRepo.findById(userId) // Option<User> .flatMap(User::primaryAddress) // Option<Address> .filter(Address::isVerified) // discard unverified .map(AddressFormatter::format) // Option<String> .getOrElse("Default Warehouse, 1 Main St");}
// JPA / Spring Data repositories return Optional<T>.// Convert directly so DataAccessExceptions propagate as expected (5xx).public Option<Item> findById(Long id) { return Option.fromOptional(itemRepository.findById(id));}Each step propagates None silently.
No intermediate nulls, no nested conditionals.