Skip to content
JavaAgentic

Type at least two characters. Try “RAG”, “pgvector” or “tool calling”.

Optional: Correct Use and Common Abuse

What Optional was designed for and what it was not, the orElse versus orElseGet trap that evaluates the fallback every time, chaining with map and flatMap, and why Optional fields are a mistake.

Beginner7 min readUpdated
On this page

Optional arrived to make "this method might not return anything" visible in a signature. It works well for exactly that, and most of the criticism it attracts comes from using it for other things.

Key Takeaways

  • Designed as a return type for methods that may legitimately find nothing. Not a field, not a parameter, not a collection element.
  • orElse(x) always evaluates x. orElseGet(() -> x) evaluates it only when empty.
  • Chain with map, flatMap and filter rather than unwrapping and re-checking.
  • Never return null from a method returning Optional — that is the worst of both designs.
  • An empty collection already means "nothing"; wrapping one in an Optional adds a state nobody needs.

What it is for

the signature tells the truth
// Before: does this return null? The signature does not say. The javadoc might.
public Customer findByEmail(String email);
 
// After: the caller cannot ignore the empty case — it will not compile.
public Optional<Customer> findByEmail(String email);

That is the entire value proposition. It moves a fact from documentation, where it is ignored, into the type system, where the compiler enforces it.

using it
String displayName = customers.findByEmail(email)
        .map(Customer::name)                       // Optional<String>
        .filter(name -> !name.isBlank())
        .orElse("Unknown customer");

No null check appears anywhere, and there is no path through this code that produces a NullPointerException.

orElse versus orElseGet

This is the most-asked Optional question, and it has a real cost behind it.

the trap
public Config load(String key) {
    return cache.find(key)
            .orElse(loadFromDatabase(key));     // ALWAYS runs the database call
}
 
public Config loadCorrectly(String key) {
    return cache.find(key)
            .orElseGet(() -> loadFromDatabase(key));   // only on a cache miss
}

orElse takes a value. Java evaluates arguments before the call, so loadFromDatabase(key) runs whether or not the cache had the entry — and on a hit, its result is computed and discarded. With a 95% hit rate you have built a cache that queries the database on every request.

MethodArgumentEvaluated
orElse(T)A valueAlways
orElseGet(Supplier<T>)A supplierOnly when empty
orElseThrow()Throws NoSuchElementException when empty
orElseThrow(Supplier<X>)An exception supplierOnly when empty

The rule: use orElse for a constant or an already-computed value; orElseGet for anything that does work.

Chaining

map, flatMap and filter
// map: the function returns a plain value
Optional<String> city = findCustomer(id).map(Customer::address).map(Address::city);
 
// flatMap: the function itself returns an Optional — avoids Optional<Optional<T>>
Optional<Manager> manager = findEmployee(id).flatMap(Employee::manager);
//                                                    ^ returns Optional<Manager>
 
// filter: keeps the value only if the predicate passes
Optional<Order> largeOrder = findOrder(ref).filter(o -> o.total().compareTo(LIMIT) > 0);
 
// or(): fall back to another Optional, lazily (Java 9)
Optional<Config> config = fromEnvironment(key).or(() -> fromFile(key)).or(() -> fromDefaults(key));
 
// ifPresentOrElse: two branches, no unwrapping (Java 9)
findCustomer(id).ifPresentOrElse(
        this::sendWelcomeEmail,
        () -> log.warn("no customer for {}", id));
 
// stream(): flatten a stream of Optionals (Java 9)
List<Customer> found = ids.stream().map(this::findCustomer).flatMap(Optional::stream).toList();

map versus flatMap is the standard follow-up, and the answer is mechanical: if the function you are applying already returns an Optional, use flatMap, otherwise you end up with Optional<Optional<T>>.

The anti-patterns

six ways to misuse it
// 1. isPresent + get — this is the null check Optional was meant to replace
if (opt.isPresent()) { use(opt.get()); }
opt.ifPresent(this::use);                       // better
 
// 2. Optional as a field: not Serializable, extra allocation per instance
public class Customer { private Optional<String> phone; }   // use a nullable String
 
// 3. Optional as a parameter: three states for the caller instead of one
public void search(String term, Optional<String> region);   // overload instead
 
// 4. Optional wrapping a collection: empty list already means "none"
public Optional<List<Order>> findOrders(String id);         // return an empty List
 
// 5. Returning null from an Optional method — the worst of both worlds
public Optional<X> find() { return null; }                  // return Optional.empty()
 
// 6. Optional.of on something that might be null
return Optional.of(map.get(key));                           // NPE — use ofNullable

Number two deserves the detail because it is the most argued about. Optional does not implement Serializable, so an Optional field breaks Java serialisation outright and confuses JPA, Jackson and most mapping frameworks. It also costs one extra object per field per instance. A nullable field with an Optional-returning getter gives you the API benefit with none of the cost:

the accepted compromise
public class Customer {
    private String phone;                        // nullable field
 
    public Optional<String> phone() {            // Optional at the API boundary
        return Optional.ofNullable(phone);
    }
}

Number six is worth a habit: Optional.of throws if its argument is null, Optional.ofNullable returns empty. Use of when null would be a bug you want to hear about, ofNullable when null is expected.

Optional and Spring Data

Spring Data supports Optional return types directly, which is where most Java developers meet it:

CustomerRepository.java
public interface CustomerRepository extends JpaRepository<Customer, Long> {
    Optional<Customer> findByEmail(String email);
}
the idiomatic service method
@Transactional(readOnly = true)
public CustomerView get(String email) {
    return repository.findByEmail(email)
            .map(CustomerView::from)
            .orElseThrow(() -> new CustomerNotFoundException(email));
}

Three lines, no null check, and a specific exception that a @ControllerAdvice can turn into a 404. This is Optional doing exactly the job it was designed for, and it is a good example to volunteer when asked where you have used it.

Performance, honestly

Every Optional is an object allocation, except Optional.empty() which is a shared singleton. On a hot path returning millions of values per second that is measurable, which is why the JDK provides OptionalInt, OptionalLong and OptionalDouble — and why Stream.findFirst returning an Optional is a cost the JDK accepted deliberately.

In ordinary application code — a repository call, a configuration lookup, an HTTP handler — the allocation is noise next to the work being done, and escape analysis often removes it entirely. Do not avoid Optional for performance reasons without a profile that says to.

Optional does not replace validation

A recurring misuse is treating Optional as a general error-handling type. It carries exactly one bit of information — present or absent — and no reason. A method that can fail for five distinct reasons and returns Optional.empty() for all of them has thrown away everything the caller needed to respond correctly.

The distinction to hold onto is that an empty Optional should mean "there is legitimately no value here", not "something went wrong". A lookup that finds no customer is the first; a lookup that failed because the database was unreachable is the second, and belongs as an exception. When a caller genuinely needs a reason, the answer is a result type — a sealed interface with Found and NotFound(reason) variants — rather than stretching Optional to carry information it has no room for.

The same argument explains why Optional is a poor fit for a method that returns a collection. Optional<List<Order>> has three states — absent, present-and-empty, present-with-items — where the domain only has two, and every caller has to handle the extra one. Returning an empty list is both simpler and impossible to misuse.

Interoperating with frameworks

Jackson serialises Optional correctly only with the jdk8 module registered, and by default an empty Optional becomes null in JSON rather than being omitted. Spring Data supports Optional return types on repository methods natively. Spring MVC accepts Optional on @RequestParam and @PathVariable, which is one of the few places an Optional parameter is idiomatic, because the framework constructs it rather than a caller.

JPA is the notable exception: an Optional entity field will not map, because Hibernate needs to read and write the field directly. The nullable-field-with-Optional-getter pattern above is the standard resolution, and it is worth naming in an interview because it shows you know where the guidance stops applying.

What gets asked

Almost always: "what is the difference between orElse and orElseGet?" Then "why shouldn't Optional be a field?" Then something about map versus flatMap. Having a real example of the orElse trap — a cache that queried the database on every hit — makes the first answer memorable rather than recited.

Frequently Asked Questions

What is the difference between orElse and orElseGet?
orElse takes a value, so its argument is evaluated before the call regardless of whether the Optional is present. orElseGet takes a Supplier, which is only invoked when the Optional is empty. If the fallback is a constant they are equivalent; if it is a database call, a new object or anything with a side effect, orElse runs it every single time — including on the happy path where the result is thrown away.
Why should Optional not be used as a field or a method parameter?
Optional is not Serializable, so an Optional field breaks serialisation and most persistence frameworks. It also adds an allocation per field on every instance. For parameters, an Optional argument gives callers three states to handle — present, empty, and null Optional — where a nullable parameter or an overload gives them one clear choice. Optional was designed as a return type and is documented that way.
Is calling Optional.get() ever acceptable?
Only immediately after isPresent() in the same expression, which is exactly the pattern Optional exists to replace, or when you can prove the value is present and want a clear failure otherwise — in which case orElseThrow() says so explicitly and is the better spelling. Since Java 10 orElseThrow() with no arguments is the direct replacement for get().

Related tutorials