Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is using Mono<Optional<T>> discouraged?

I am writing a web server with extensive usage of reactive programming. I noticed that I forgot to check if Mono is empty too many times. I am using WebFlux, so it converts an empty Mono to a 200 OK response, and it makes it very hard to detect these errors.

One way of reducing these mistakes is to make absence of a value explicit by using Mono<Optional<T>> instead of Mono.empty().

This feels very similar to Optional and null debate, it even uses the same class. And while there are many people that are in favour of using Optional and it is widely supported by libraries, I've yet to see anyone using Mono<Optional<T>>.

Are there any drawbacks of using Mono<Optional<T>>?

What is a better way to reliably handle cases of absent values?

like image 480
Thresh Avatar asked Oct 20 '25 16:10

Thresh


2 Answers

By using Mono<Optional<T>> instead of Mono<T>, you're putting yourself into a tricky situation of receiving an empty Mono, but assuming that it will always have an instance of Optional, so it doesn't really help, and only adds an unnecessary allocation.

There is a handy operator Mono#single, it will throw and error if you expected a non-empty Mono, but got an empty one instead.

like image 161
bsideup Avatar answered Oct 23 '25 06:10

bsideup


I have found a use case where Mono<Optional<T>> is quite useful.

I have several database repositories where I search various entities. Originally when no entity was found, I returned Mono.empty() :

public class PersonRepository {
    public Mono<Person> findPersonById(UUID id) {
        return ...;
    }
}

I have another code when I need to call these several methods and merge their result:

Mono.zip(
    personRepository.findPersonById(id1),
    companyRepository.findCompanyById(id2),
    carRepository.findCarId(id3)
)
// Now combine all the results together
.map((Tuple3<Person, Company, Car> data) -> mapper.map(
    data.getT1(), 
    data.getT2(), 
    data.getT3())
// and do something with the result
.flatMap(mergedData -> someConnector.send(mergedData))

The problem with Mono.zip() is - according to the documentation - that

An error or empty completion of any source will cause other sources to be cancelled and the resulting Mono to immediately error or complete, respectively.

And I want to collect the data even if some of the objects are not found. Hence I have to ensure that nothing passed to the Mono.zip() returns Mono.empty().

One of the ways to achieve it is of course "catch the empty monos" like

Mono.zip(
    personRepository.findPersonById(id1)
        .swichIfEmpty(provide some empty person),
    companyRepository.findCompanyById(id2)
        .swichIfEmpty(provide some empty company),
    carRepository.findCarId(id3)
        .swichIfEmpty(provide some empty car)
)

which is simply ugly.

Here comes the use case for Mono<Optional<T>>. Change the signature of all the getSomethingById() methods to Mono<Optional<T>>. There is even a nice utility method singleOptional()

Wrap the item produced by this Mono source into an Optional or emit an empty Optional for an empty source.

public class PersonRepository {
    public Mono<Optional<Person>> findPersonById(UUID id) {
        return ...
        .singleOptional();
    }
}

The usage of the methods will then remain clean and readable as

``` lang-java
Mono.zip(
    personRepository.findPersonById(id1),
    companyRepository.findCompanyById(id2),
    carRepository.findCarId(id3)
)
// Now combine all the results together
.map((Tuple3<Optional<Person>, Optional<Company>, Optional<Car>> data) -> mapper.map(
    data.getT1().orElse(null), 
    data.getT2().orElse(null), 
    data.getT3().orElse(null))
// and do something with the result
.flatMap(mergedData -> someConnector.send(mergedData))
like image 29
Honza Zidek Avatar answered Oct 23 '25 06:10

Honza Zidek