🕐 Reading time: 2 minutes

When working with WebFlux, it's common to handle chains of asynchronous operations. Sometimes, though, you might run into unusual outputs like Mono<Mono<Void>>. An example? Let's assume we have a startCar(keys) method that, given a car's keys (sampleCar), starts it (changing its state from STOPPED to RUNNING) and returns a Mono<Void>. To actually apply this state change, you need to subscribe to the Mono, either explicitly or implicitly.

📌 Example with explicit subscribe()

final CarState actualState = Mono.just(carKeys)
    .doOnNext(keys -> sampleCar.startCar(keys).subscribe())
    .map(keys -> sampleCar.getState())
    .block();
assertEquals(CarState.RUNNING, actualState);

⚠️ Watch out: if we had written .doOnNext(sampleCar::startCar) without calling subscribe(), the car wouldn't actually have been started, the Mono<Void> would have stayed inactive, the action wouldn't have run, and the assertEquals would have failed.

📌 Example with implicit subscription

Alternatively, you can subscribe implicitly by "flattening" the Mono<Mono<Void>> into a Mono<Void> with flatMap, which handles the subscription implicitly, and then using the then() operator to chain a follow-up operation onto the previous Mono.

final CarState actual = Mono.just(carKeys)
    .flatMap(sampleCar::startCar)
    .then(Mono.defer(() -> Mono.just(sampleCar.getState())))
    .block();

📑 Visualising how the types evolve along the chain

Let's look in detail at how the types transform along the reactive pipeline – handy for understanding why the subscription is needed and when you end up with a Mono<Mono<Void>>:

@Test
void test_subscribe() {
    final String licensePlate = "BMT 216A";
    final Car sampleCar = new Car(licensePlate);

    Mono.just(carKeys)                    //Mono<String>
            .map(sampleCar::startCar)     //Mono<Mono<Void>>
            .map(Mono::subscribe)         //Mono<Disposable>
            .map(disposable -> sampleCar) //Mono<Car>
            .map(Car::getState)           //Mono<CarState>
            .doOnNext(actual -> assertEquals(CarState.RUNNING, actual))
            .block();
}

🔍 Extra considerations

Mono.defer(() -> Mono.just(...)) is used to defer the creation of the Mono until subscription time, ensuring that logic depending on the current state runs correctly.

See you at the next pill! ☕