3

So, I'm trying to work with Webflux and I've got a scenario "check if an object exists; if so, do stuff, else - indicate error".

That can be written in reactor as:

public Mono<Void> handleObjectWithSomeId(Mono<IdType> id){
    return id.
        flatMap(repository::exists). //repository.exists returns Mono<Boolean>
        flatMap(e -> e ? e : Mono.error(new DoesntExistException())). 
        then(
            //can be replaced with just(someBusinessLogic())
            Mono.fromCallable(this::someBusinessLogic) 
        );
}

or as:

public Mono<Void> handleObjectWithSomeId(Mono<IdType> id){
    return id.
        flatMap(repository::exists). //repository.exists returns Mono<Boolean>
        flatMap(e -> e ? e : Mono.error(new DoesntExistException())). 
        map(e -> this.someBusinessLogic()));
}

Let's assume that return type of someBusinessLogic cannot be changed and it has to be simple void, not Mono<Void>.

In both cases if the object won't exist, appropriate Mono.error(...) will be produced.

While I understand that then and flatMap have different semantics, effectively I get the same result. Even though in second case I'm using flatMap against its meaning, I get to skip flatMap and fromCallable in favor of simple map with ignored argument (that seems more readable). My point is, both apporaches have advantages and disadvantages when it comes to readability and code quality.

So, here's a summary:

Using then

  • pros
    • is semantically correct
  • cons
    • in many cases (like above) requires wrapping in ad-hoc Mono/Flux

Using flatMap

  • pros
    • simplifies continued "happy scenario" code
  • cons
    • is semantically incorrect

What are other pros/cons of both approaches? What should I take under consideration when choosing an operator?

I've found this reactor issue that states that there is not real difference in speed.

Filip Malczak
  • 3,124
  • 24
  • 44

1 Answers1

11

TL, DR: If you care about the result of the previous computation, you can use map(), flatMap() or other map variant. Otherwise, if you just want the previous stream finished, use then().

You can see a detailed log of execution for yourself, by placing an .log() call in both methods:

public Mono<Void> handleObjectWithSomeId(Mono<IdType> id) {
    return id.log()
             .flatMap(...)
             ...;
}

Like all other operations in Project Reactor, the semantics for then() and flatMap() are already defined. The context mostly defines how these operators should work together to solve your problem.

Let's consider the context you provided in the question. What flatMap() does is, whenever it gets an event, it executes the mapping function asynchronously.

flatmap marble diagram

Since we have a Mono<> after the last flatMap() in the question, it will provide the result of previous single computation, which we ignore. Note that if we had a Flux<> instead, the computation would be done for every element.

On the other hand, then() doesn't care about the preceding sequence of events. It just cares about the completion event:

enter image description here

That's why, in your example it doesn't matter very much which one you use. However, in other contexts you might choose accordingly.

You might also find the Which operator do I need? section Project Reactor Reference helpful.

MuratOzkan
  • 2,599
  • 13
  • 25
  • Your explanation is very well phrased and I think some people will find it helpful, but unfortunately it doesn't answer my question. I do understand how both these operators work, I was just wondering about this specific case of Mono-based error. I'm just wondering now whether there is a difference when is onComplete called in case of then() and flatMap(), like if then() completes previous stream, while flatMap would wait for next stream to complete first. – Filip Malczak Apr 10 '18 at 15:49
  • What exactly do you mean by next or previous stream? If the `onComplete` is what you're after, diagrams describe the termination events. – MuratOzkan Apr 11 '18 at 16:52
  • If you're interested in error flow, both operations will emit an error event if there's an exception, or a `Mono.error(...)` is emitted by an operation. Any errors will be caught by the `onErrorXxxx()` handlers. In any case, I suggest you put a `log()` statement in the method, to actually see what's going on. – MuratOzkan Apr 11 '18 at 16:57