# Optional is not your domain

I've gone through several phases in the way I represent absence in Java.

At first, I returned `null`.

Then I discovered `Optional`, and started returning it everywhere.

It felt like progress, and it was.

But eventually I ran into a different problem.

I had an `Optional.empty()` in front of me, and I had no idea **why it was empty**.

That was the moment I started questioning whether `Optional` belonged in my domain at all.

This article isn't about why `Optional` is bad. It isn't.

It's about taking the idea behind `Optional` one step further:

> **If the absence of a value has meaning, model that meaning.**

* * *

## Stage 1 - `null`: "Good luck"

Let's take a simple example from a pet store.

We want to map some data into a `Pet`.

```java
public Pet map(PetData data) {
    if (data == null) {
        return null;
    }

    if (data.name() == null) {
        return null;
    }

    return new Pet(data.name(), data.age());
}
```

We've all written code like this.

The method technically communicates something:

> Sometimes I give you a `Pet`. Sometimes I don't.

The problem is that this information isn't part of the contract.

```java
Pet pet = mapper.map(data);

pet.name(); // 💥
```

Nothing in the type system tells the caller that `pet` might not exist.

The responsibility for dealing with that possibility has effectively been pushed upstream, without giving the caller any help.

And there's an even bigger problem.

*   What does `null` mean?
    
*   Was `data` null?
    
*   Was the name missing?
    
*   Did validation fail?
    
*   Was there simply no pet to map?
    

We don't know.

`null` has collapsed all of those situations into the same thing.

It represents the absence of an object, but says nothing about the reason for that absence.

* * *

## Stage 2 - Exceptions: "Something went wrong!"

We can improve this by refusing to silently return `null`.

```java
public Pet map(PetData data) {
    if (data == null) {
        throw new MissingPetDataException();
    }

    if (data.name() == null) {
        throw new MissingPetNameException();
    }

    return new Pet(data.name(), data.age());
}
```

That's already better in one important way.

We haven't lost the reason anymore.

The caller can distinguish between different failures.

But now our caller starts looking like this:

```java
try {
    Pet pet = mapper.map(data);
    process(pet);
} catch (MissingPetDataException e) {
    handleMissingData();
} catch (MissingPetNameException e) {
    handleMissingName();
}
```

Exceptions have their place.

If something exceptional happened - an invariant was violated, the database disappeared, an operation couldn't continue, throwing an exception can be exactly the right thing to do.

But there's an important distinction:

**not every unsuccessful outcome is exceptional.**

If incomplete pet data is an expected state in our system, then we're using exceptions to represent normal domain behaviour.

Our method signature still says:

```java
Pet map(PetData data)
```

But its real contract is closer to:

```text
Pet
OR missing data
OR missing name
```

The signature doesn't tell us that.

We have to know which exceptions might escape from the implementation.

We've made the failure louder.

We haven't necessarily made the domain clearer.

* * *

## Stage 3 - `Optional`: "There might not be a Pet"

Then comes `Optional`.

```java
public Optional<Pet> map(PetData data) {
    if (data == null) {
        return Optional.empty();
    }

    if (data.name() == null) {
        return Optional.empty();
    }

    return Optional.of(
        new Pet(data.name(), data.age())
    );
}
```

This feels much better.

And it **is** much better than returning `null`.

The contract now explicitly says:

```java
Optional<Pet>
```

There might be a `Pet`.

There might not.

The caller cannot accidentally pretend otherwise.

We can also compose operations nicely:

```java
mapper.map(data)
    .map(this::enrich)
    .filter(this::isValid)
    .ifPresent(this::process);
```

No `null` checks.

No accidental `NullPointerException`.

No `try/catch` controlling ordinary program flow.

`Optional` solved a real problem.

But it introduced a temptation.

Because `Optional` is convenient, we start using it to represent things it doesn't actually represent.

Consider our mapper again:

```java
public Optional<Pet> map(PetData data) {
    if (data == null) {
        return Optional.empty();
    }

    if (data.name() == null) {
        return Optional.empty();
    }

    if (!isSupported(data.type())) {
        return Optional.empty();
    }

    return Optional.of(toPet(data));
}
```

Now imagine receiving:

```java
Optional.empty()
```

Why is it empty?

We don't know.

Three completely different situations have been compressed into the same technical representation.

And this was the problem I eventually ran into in real code.

I had several gates in a mapping process. Any one of them could result in `Optional.empty()`.

At the end of the pipeline, I knew that no object had been produced.

But I had lost **why**.

And that "why" was domain information.

* * *

## `Optional.empty()` can sweep information under the rug

There's another subtle consequence.

Imagine this:

```java
Optional<Pet> pet = mapper.map(data);

if (pet.isEmpty()) {
    return;
}
```

Simple.

But what decision did we just make?

Maybe the pet should be ignored because the input genuinely doesn't represent one.

Maybe the data is incomplete and should be retried later.

Maybe the pet type isn't supported and should generate a metric.

Maybe the input violates a business rule and should be reported.

Those are different behaviours.

Yet `Optional.empty()` encourages the caller to treat them as one.

Worse, the decision about those cases has effectively moved inside the mapper.

The mapper isn't merely mapping anymore.

It's validating.

It's interpreting domain rules.

And by returning `empty`, it's deciding that all those outcomes mean the same thing to the caller.

That's a lot of responsibility hidden behind a very innocent-looking return type.

* * *

## Stage 4 - Model the outcome

Modern Java gives us another option.

Instead of describing the presence or absence of a value, we can describe the **possible outcomes of the operation**.

For example:

```java
public sealed interface PetMappingResult
    permits MappedPet, MissingInformation, UnsupportedPetType {
}
```

And then:

```java
public record MappedPet(Pet pet)
    implements PetMappingResult {
}

public record MissingInformation(Set<String> fields)
    implements PetMappingResult {
}

public record UnsupportedPetType(String type)
    implements PetMappingResult {
}
```

Our mapper can now return:

```java
public PetMappingResult map(PetData data) {
    if (data.name() == null) {
        return new MissingInformation(Set.of("name"));
    }

    if (!isSupported(data.type())) {
        return new UnsupportedPetType(data.type());
    }

    return new MappedPet(toPet(data));
}
```

Look at the signature:

```java
PetMappingResult map(PetData data)
```

We're no longer saying:

> Maybe there's a Pet.

We're saying:

> Mapping a Pet has a defined set of outcomes.

That's a very different contract.

And, more importantly, those outcomes belong to our domain.

* * *

## Now the caller makes the decision

The mapper no longer needs to decide what `MissingInformation` means to the rest of the application.

It reports what happened.

The caller decides what to do with it.

```java
switch (mapper.map(data)) {
    case MappedPet(var pet) ->
        process(pet);

    case MissingInformation(var fields) ->
        scheduleForRetry(fields);

    case UnsupportedPetType(var type) ->
        recordUnsupportedType(type);
}
```

There's no `null`.

There's no `Optional.empty()` hiding the reason.

There's no `try/catch` representing expected control flow.

And the code reads surprisingly close to the domain:

```text
Mapped pet      -> process it
Missing data    -> retry it
Unsupported pet -> record it
```

The types carry the information.

* * *

## And then the domain changes

This is where I think this approach becomes particularly interesting.

Software changes.

Imagine that six months later we discover another legitimate outcome:

```java
public record TemporarilyUnavailable(String reason)
    implements PetMappingResult {
}
```

We add it to our sealed hierarchy:

```java
public sealed interface PetMappingResult
    permits MappedPet,
            MissingInformation,
            UnsupportedPetType,
            TemporarilyUnavailable {
}
```

With `Optional`, perhaps we'd have done this:

```java
return Optional.empty();
```

The existing callers would continue compiling.

Nothing would tell them that a **new domain state now exists**.

They would keep treating it exactly like every other absence.

With the sealed hierarchy and an exhaustive pattern-matching `switch`, something very useful happens.

The compiler complains.

Our existing code:

```java
switch (result) {
    case MappedPet(var pet) -> process(pet);
    case MissingInformation(var fields) -> scheduleForRetry(fields);
    case UnsupportedPetType(var type) -> recordUnsupportedType(type);
}
```

is no longer exhaustive.

We have introduced a new state into the domain without defining how the application should handle it.

That's exactly the kind of problem I **want** the compiler to find.

The compiler has effectively become part of the domain modelling process.

* * *

## So is returning `Optional` bad?

No.

`Optional` is a great Java abstraction.

I still use it.

A lot.

Inside an implementation, it can be an excellent tool.

```java
Optional.ofNullable(rawPet)
    .map(this::normalize)
    .filter(this::isUsable)
    .map(this::convert);
```

It lets us express transformations elegantly without scattering `null` checks throughout the code.

And sometimes absence really **is** the complete semantic information.

Consider:

```java
Optional<Pet> findById(PetId id);
```

Depending on the domain, that may be exactly the contract we want:

> There either is a Pet with this ID, or there isn't.

There's no additional information to preserve.

In that situation, inventing a hierarchy of domain types would probably add ceremony without adding meaning.

The problem isn't `Optional`.

The problem is using `Optional` when `empty` actually represents several meaningful states.

* * *

## Technical abstractions aren't automatically domain abstractions

This is the distinction I've gradually started making in my own code.

`Optional` belongs to Java.

Your domain belongs to your application.

Sometimes those abstractions happen to line up perfectly.

Sometimes they don't.

The fact that Java gives us:

```java
Optional<Pet>
```

doesn't mean our domain contains the concept of an "optional pet".

Maybe it contains:

```text
Pet mapped
Pet information incomplete
Pet type unsupported
Pet temporarily unavailable
```

Those are different concepts.

Compressing all of them into:

```java
Optional.empty()
```

doesn't simplify the domain.

It removes information from it.

* * *

## A progression, not a rule

I don't see these approaches as four competing camps.

For me, they represent a progression in how I thought about responsibility.

I started with:

```java
Pet
```

and sometimes secretly returned `null`.

Then I moved to:

```java
Optional<Pet>
```

and made absence explicit.

That was an improvement.

But eventually I realised that sometimes I was using `Optional.empty()` in exactly the same way I'd previously used `null`.

I'd made the absence type-safe.

I still hadn't explained it.

Today, when designing a return type, I try to ask a slightly different question.

Not:

> **Can this method return nothing?**

But:

> **What are the meaningful outcomes of this operation?**

Sometimes the answer genuinely is:

```java
Optional<Pet>
```

And that's perfectly fine.

But when `empty` starts meaning *missing information*, *unsupported*, *invalid*, *not ready yet*, or *something else happened*...

I probably don't have an optional value anymore.

I have a domain model waiting to be written.
