Capa do artigo: Optional in Java: monad and computational absence

Optional in Java: monad and computational absence

Before talking about Optional, it is worth making explicit a concept from functional programming that lies behind the type. That concept is the monad.

Monads in a nutshell

A monad is a pattern that wraps a value in a context and offers a standardized way to chain operations on that value without having to "unwrap" the context at each step. The context can be "a value that might exist", "a value that might fail", "a list of values", or "a deferred side effect". What matters is that operations compose predictably and the context is preserved throughout the chain.

In languages like Haskell, monads are a central part of the type system. In Java, the concept appears implicitly. Stream, CompletableFuture, and Optional follow the same pattern. Each one wraps a value in a different context (collection, asynchronous computation, presence or absence) and offers map and flatMap to chain transformations without leaving the context.

flowchart LR
    subgraph Context
        A[value] --> B[map / flatMap]
        B --> C[new value in the same context]
    end

What makes the pattern useful in practice is that the code consuming the monad does not need to check the context at each operation. The check happens inside the type itself. If the Optional is empty, map does not execute the function. If the Stream has no elements, the pipeline simply produces no result. The context takes care of itself.

Optional as the Maybe monad

Optional, since Java 8, materializes in Java the monad that in other languages is usually called Maybe (Haskell) or Option (Scala, Rust). A container that either holds a value of type T or represents the absence of that value. Presence or absence is part of the type itself, and the code consuming an Optional handles both possibilities through the API, instead of relying on null and manually scattered checks.

Optional<User> userOpt = repository.findById(id);

String formattedName = userOpt
    .map(User::getName)
    .filter(name -> !name.isBlank())
    .map(String::toUpperCase)
    .orElse("UNKNOWN");

map applies a function to the inner value only when the Optional is present. filter turns a present Optional into an empty one when the predicate fails. orElse resolves the final value or provides a default. No null appears in the flow. Anyone reading the code can see that absence is a case handled by the type's own structure.

When the next step in the composition returns another Optional, flatMap comes into play. Without it, a map returning an Optional would produce Optional<Optional<T>>. flatMap flattens one level, keeping a single Optional in the chain.

Optional<Address> address = userOpt
    .flatMap(User::getPrimaryAddress);

The diagram below summarizes the flow. If the Optional is empty, the result of any operation remains empty. If it is present, the function is applied and the result continues in the chain.

flowchart LR
    subgraph Input
        A[empty Optional]
        B[Optional with value]
    end

    A --> C[map / flatMap / filter]
    B --> C

    C --> D[empty Optional]
    C --> E[Optional with value]

In terms of category theory, Optional fulfills the laws expected of a monad (left and right identity, associativity of composition). For day-to-day Java, it is enough to keep in mind that Optional is a functional composition tool that maintains the "present or absent value" context throughout operations.

Where Optional fits

The official Java platform documentation is straightforward about the intended use.

"Optional is primarily intended for use as a method return type where there is a clear need to represent 'no result,' and where using null is likely to cause errors. A variable whose type is Optional should never itself be null; it should always point to an Optional instance."
— java.util.Optional, API Note (Java 21)

Brian Goetz, the language architect, reinforced the intended scope directly.

"Of course, people will do what they want. But we did have a clear intention when adding this feature, and it was not to be a general purpose Maybe type, as much as many people would have liked us to do so. Our intention was to provide a limited mechanism for library method return types where there needed to be a clear way to represent 'no result,' and using null for such was overwhelmingly likely to cause errors."
— Brian Goetz, StackOverflow

A repository that looks up an entity by ID can return Optional<Entity>. A service that queries an external resource can return Optional<Response>. The method signature communicates that the caller must consider the case where there is no result. The same applies to functional transformations where each step can produce absence. Query APIs, integrations with external systems, and pipelines that depend on lookups or parsing are natural scenarios.

public interface ProductRepository {
    Optional<Product> findBySku(String sku);
}

public class CatalogService {
    public String formattedDescription(String sku) {
        return productRepository.findBySku(sku)
            .map(Product::description)
            .map(String::trim)
            .filter(d -> !d.isBlank())
            .orElse("No product description");
    }
}

Where Optional does not fit

Stuart Marks, an Oracle engineer and member of the OpenJDK team, systematized recommended practices in a presentation that became a reference. Among the rules he documented, one is particularly relevant here.

"Avoid using Optional in fields, method parameters, and collections."
— Stuart Marks, Optional: The Mother of All Bikesheds (Devoxx BE, 2016)

Java itself discourages using Optional as a field. The Optional class does not implement Serializable. Placing Optional<Something> as an attribute of a JPA entity or a DTO bound for JSON introduces complications that the type was not designed to solve. Serialization frameworks and object-relational mapping were not built with Optional as part of the persisted structure.

// Problematic usage: Optional as a field
public class Customer {
    private final String name;
    private final Optional<String> phone; // not serializable, complicates JPA and JSON

    // ...
}
// Direct alternative: null in the field, Optional in the return
public class Customer {
    private final String name;
    private final String phone; // can be null

    public Optional<String> getPhone() {
        return Optional.ofNullable(phone);
    }
}

In the second approach, the internal field is null when there is no phone number. Optional appears only in the method return, exactly where it was designed to operate. The serialization framework handles null normally, and the API consumer receives an Optional that communicates the possibility of absence.

Optional as a method parameter also creates problems. The caller needs to decide whether to pass Optional.of(value), Optional.empty(), or (worse) null. The receiving method needs to handle all these possibilities. An overload or a nullable parameter tends to be simpler.

// Problematic: Optional as a parameter
public List<Product> search(Optional<String> category) {
    // The caller can pass null, Optional.empty(), or Optional.of(...)
}

// More direct: overload
public List<Product> search() { ... }
public List<Product> search(String category) { ... }

For collections, the rule is even clearer. An empty collection already represents the absence of elements. Wrapping a list in Optional adds a layer of indirection with no semantic gain.

Null in Java: when it makes sense

The existence of Optional does not make null obsolete. Null still has its place in Java, and understanding where is the key to avoiding both overuse of Optional and the NullPointerExceptions that motivated its creation.

Null works well as internal state of an object. Private fields that may not have a value are a legitimate use. The class's encapsulation ensures that null does not leak without handling. The getter can return Optional, converting the internal null into an explicit type at the public boundary.

Null also works as a representation of absence in structures that will be serialized. JSON has the native concept of an absent or null field. JPA maps null to nullable columns. These technologies were designed around null, and forcing Optional in this context creates friction without benefit.

Where null causes problems is in public method returns. A method that returns null to indicate "not found" depends on every caller remembering to check. If a caller forgets, the result is a NullPointerException at some distant point in the code. Optional solves exactly this problem by making the check part of the type.

// Null in the return: the caller may forget to check
Product product = repository.findBySku("ABC-123");
String name = product.getName(); // NullPointerException if not found

// Optional in the return: absence is part of the type
Optional<Product> product = repository.findBySku("ABC-123");
String name = product
    .map(Product::getName)
    .orElse("Not found"); // absence handled in the composition

Kotlin and the type system alternative

Kotlin addresses the same problem differently. Instead of a wrapper type like Optional, the language incorporated the distinction between nullable and non-nullable directly into the type system.

"In Kotlin, the type system distinguishes between types that can hold null (nullable types) and those that cannot (non-nullable types)."
— Kotlin Documentation, Null Safety

A String variable does not accept null. A String? variable does. The compiler checks this at compile time. There is no way to assign null to a non-nullable type without the compiler rejecting the code.

var name: String = "Ana"      // does not accept null
var nickname: String? = null  // accepts null

name = null    // compilation error
nickname = null // allowed

The safe call operator ?. allows chaining operations on nullable types without risk of NullPointerException. If the value on the left is null, the entire expression evaluates to null without throwing an exception.

"The safe call operator ?. allows you to handle nullability safely in a shorter form. Instead of throwing an NPE, if the object is null, the ?. operator simply returns null."
— Kotlin Documentation, Null Safety

The Elvis operator ?: provides a default value when the expression on the left is null. It is the functional equivalent of Optional's orElse, but without the wrapper layer.

"If the expression to the left of ?: is not null, the Elvis operator returns it. Otherwise, the Elvis operator returns the expression to the right."
— Kotlin Documentation, Null Safety

val user: User? = repository.findById(id)

val formattedName = user
    ?.name
    ?.takeIf { it.isNotBlank() }
    ?.uppercase()
    ?: "UNKNOWN"

Compare with the Java equivalent.

Optional<User> user = repository.findById(id);

String formattedName = user
    .map(User::getName)
    .filter(name -> !name.isBlank())
    .map(String::toUpperCase)
    .orElse("UNKNOWN");

The result is the same. The difference lies in the level at which the solution operates. Kotlin solves it in the type system. The compiler guarantees that no non-nullable type receives null. Java solves it at the library level. Optional is a class that wraps the value and offers a composition API. The guarantee in Java depends on the discipline of using Optional where appropriate, since null still exists in the language.

This difference has practical consequences. In Kotlin, a data class field can be String? and the compiler forces handling at every access. In Java, a String field in a record can be null and nothing prevents access without checking. Optional in the getter's return mitigates the problem at the public boundary, but the internal null continues to exist.

Computational absence versus domain state

It is worth separating two planes that often get confused when Optional enters the conversation.

Optional models what can be called computational absence: a condition that arises during processing. A method that searches, queries a repository, or transforms an external input may or may not produce a result. This uncertainty belongs to the flow of computation. Optional represents exactly this kind of operational uncertainty.

In the business domain, the situation is different. The domain model describes real states of the system. A customer may or may not have a registered phone number. An order may or may not have an applied coupon. A user may or may not have an address. These situations are part of the representation of the world being modeled.

In data-oriented approaches, as discussed in the articles on multiparadigm Java and Data-Oriented Programming, absence in the domain is usually expressed directly in the type structure. A sealed hierarchy makes variants explicit and verifiable by the compiler.

public sealed interface Contact
    permits WithPhone, WithoutPhone {}

public record WithPhone(String number) implements Contact {
    public WithPhone {
        if (number == null || number.isBlank())
            throw new IllegalArgumentException("Number cannot be empty");
    }
}

public record WithoutPhone() implements Contact {}
String message = switch (customer.contact()) {
    case WithPhone(var number) -> "Send SMS to " + number;
    case WithoutPhone() -> "Notify by email";
};

In this model, the absence of a phone number is neither null nor Optional.empty(). It is a type with a name, with semantics, and with exhaustive handling guaranteed by the compiler. Absence is part of the domain's vocabulary. Compare this with Optional<String> phone, where "absent" and "present" are the only possible states and the concept's name is lost inside a generic type.

The choice between approaches depends on context. Optional works well when absence is an operational detail of the flow (a search that may fail, parsing that may not find a match). Modeling with types works better when absence is part of the domain's meaning and needs differentiated handling.

Conclusion

Optional in Java is a monad that implements the Maybe pattern. It enables functional composition through map, flatMap, and filter, avoids null in method returns, and maintains the "present or empty" context throughout the chain. The Java documentation and the language architects are clear about the intended scope. Optional was designed for method returns. Using it as a field, parameter, or collection wrapper falls outside that design and tends to create more problems than it solves.

Null still has its place. Internal fields, serializable structures, and contexts where the framework expects null remain legitimate. The existence of Optional does not invalidate null. It delineates where each one operates more clearly.

Kotlin shows that the same problem can be solved differently, with nullability incorporated into the type system instead of handled by a wrapper class. Each approach reflects a language design decision. Java chose to maintain compatibility with decades of existing code and offer Optional as a functional composition tool. Kotlin, without the weight of backward compatibility, incorporated null safety into the compiler.

When absence is operational (lookups, queries, parsing), Optional fits. When absence is part of the domain's vocabulary, sealed types and explicit modeling tend to express intent better. Understanding this distinction is what transforms Optional from a syntactic detail into a design choice.

References

Brian Goetz. Answer about Optional on StackOverflow. StackOverflow, 2014.

Stuart Marks. Optional: The Mother of All Bikesheds. Devoxx BE, 2016.

Oracle. java.util.Optional (Java 21). Java Platform SE Documentation.

Kotlin Documentation. Null Safety. kotlinlang.org.