Search code examples
javareactive-programmingspring-webfluxproject-reactor

Handle nested conditions in Spring Reactor


I have 3 checks which I need to make in an incremental way.

Mono<Boolean> isRegistered(Student std);
Mono<Boolean> isEligible(Student std);
Mono<Boolean> isAvailable(Student std); 

Each method performs a check inside and returns true / false

I want a logic that will stop the flow and throw error if any of the checks return false something like this:

Mono<Boolean> checkAll(Student std) {
return isRegistered(std) && isEligible(std) && isAvailable(std);
}

I tried with Mono.zip() but it allows only 2 params, also it does not allow to run 2nd condition only after 1st condition is true.

I also tried with Mono.defer().then() like this

return Mono.defer(() -> 

         isRegistered(std))
        .then(Mono.defer(() -> isEligible(std))
        .then(Mono.defer(() ->isAvailable(std));

but the problem is it returns true if any one of the condition is true

I want to call the 2nd method only if first method is true.


Solution

  • The naive approach is to evaluate conditions sequentially using flatMap and complete sequence (return Mono.empty) in case some condition is false.

    condition1()
        .flatMap(res -> {
            if (!res) {
                return Mono.empty();
            }
            return condition2();
        })
        .flatMap(res -> {
            if (!res) {
                return Mono.empty();
            }
            return condition3();
        })
        .flatMap(res -> {
            if (!res) {
                return Mono.empty();
            }
            return action(); // if all conditions true
        })
        .switchIfEmpty(otherAction()); // else
    

    You can also use Mono.zip that supports up to 8 publishers or you can pass Iterable if you need more. zip subscribes eagerly (to all publishers) and wait until all conditions are evaluated. We can overcome the second restriction by using the fact that empty completion of any source will cause other sources to be cancelled and the resulting Mono to immediately complete.

    Mono.zip(emptyIfFalse(condition1()), emptyIfFalse(condition2()), emptyIfFalse(condition3()))
        .flatMap(__ -> action()) // all conditions TRUE
        .switchIfEmpty(otherAction()); // some condition FALSE
    
    private Mono<Boolean> emptyIfFalse(Mono<Boolean> condition) {
        return condition
                .filter(res -> res);
    }
    

    In this case all conditions are still evaluated in parallel (that could be an advantage in some cases) but sequence will complete on the first false.

    Another approach is to use Flux.concat that sequentially subscribes to the first source then waiting for it to complete before subscribing to the next.

    Flux.concat(condition1(), condition2(), condition3())
        .filter(res -> !res)
        .next() // emit first FALSE and stop
        .flatMap(__ -> otherAction()) // some condition FALSE
        .switchIfEmpty(action()); // all conditions TRUE
    

    UPDATE

    To achieve logical "or" condition with the above example (return true if any of the condition is true, and return false when all conditions are false)

    Flux.concat(condition1(), condition2(), condition3())
        .filter(res -> res)
        .next() // emit first TRUE and stop
        .flatMap(__ -> action()) // some condition TRUE
        .switchIfEmpty(otherAction()); // all conditions FALSE