Optional<T> shipped in Java 8 with a narrow, explicit purpose, stated plainly in its own Javadoc: a return type for methods that might legitimately have no result, used to make the possibility of absence visible in the method signature instead of buried in documentation nobody reads. It was never intended as a general-purpose null-safety wrapper, and most of the complaints about Optional being clunky come from using it somewhere the design never covered.

What It's Actually For

java
public Optional<User> findByEmail(String email) {
    return userRepository.findByEmail(email)
        .map(this::toDomainUser);
}

The caller can see, from the type alone, that "no user with this email" is a normal, expected outcome, not an edge case they'll discover the hard way with a NullPointerException three calls deep. That's the whole value proposition: pushing "this might not exist" into the type system at the API boundary.

java
User user = userService.findByEmail(email)
    .orElseThrow(() -> new UserNotFoundException(email));

Where It Doesn't Belong

Fields. Optional is not Serializable in any dependable way, and wrapping a field in it doubles the allocation for something a plain nullable reference already represents. Use a nullable field, or better, avoid the nullable state entirely via the builder pattern or a required constructor argument.

java
// Avoid — adds an allocation and API friction for no real benefit
public class Customer {
    private Optional<String> middleName;
}

// Prefer — plain nullable field, or restructure to avoid the null entirely
public class Customer {
    private String middleName; // may be null, and that's fine for internal state
}

Method parameters. Forcing every caller to wrap arguments in Optional.of(...) just to call your method adds ceremony without adding safety — nothing stops someone from passing null as the Optional itself, which reintroduces exactly the bug Optional exists to prevent, at one more level of indirection.

java
// Awkward for every caller, and doesn't actually prevent null
void schedule(Optional<LocalDate> date) { ... }

// Overloads or a sentinel/default make the intent clearer
void schedule(LocalDate date) { ... }
void scheduleUnspecified() { ... }

Collections. An empty List or Map already represents "nothing here" without an extra wrapper. Optional<List<T>> is redundant — return an empty list instead of Optional.empty(), and let callers iterate without unwrapping first.

The isPresent()/get() Anti-Pattern

The single most common Optional misuse is using it exactly like a null check, which throws away everything the functional-style API was for:

java
// Defeats the purpose — this is just a null check with extra syntax
if (userOpt.isPresent()) {
    User user = userOpt.get();
    sendWelcomeEmail(user);
}

// Idiomatic — expresses the same logic without ever unwrapping manually
userOpt.ifPresent(this::sendWelcomeEmail);
Instead of Prefer
if (opt.isPresent()) opt.get()... opt.ifPresent(...)
opt.isPresent() ? opt.get() : fallback opt.orElse(fallback)
opt.get() after an unrelated null-guard opt.orElseThrow(...) with a meaningful exception
Manual null-check-then-transform opt.map(...).orElse(...)

Chaining Instead of Unwrapping Early

Optional's real strength shows up when you compose several optional-returning steps without unwrapping between them:

java
String city = findUser(id)
    .map(User::address)
    .map(Address::city)
    .orElse("Unknown");

Each map short-circuits automatically if any prior step was empty — no nested null checks, no early returns scattered through the method. Reach for Optional specifically for this composability at API boundaries, keep it out of fields, parameters, and collections, and stop calling .get() — if you're calling it, you probably didn't need Optional in the first place.