How to deal with Completion Stages?

Learn how to interact with CompletionStages and CompletableFutures.

CompletionStage and CompletableFuture are classes provided by Java to represent asynchronous actions.

Differences between Uni and CompletionStage

While CompletionStage and CompletableFuture are close to Uni in terms of use case, there are some fundamental differences.

CompletionStage are eager. When a method returns a CompletionStage, the operation has already been triggered. The outcome is used to complete the returned CompletionStage. On the other side, Unis are lazy. The operation is only triggered once there is a subscription.

CompletionStage caches the outcome. So, once received, you can retrieve the result. Every retrieval will get the same result. With Uni, every subscription has the opportunity to re-trigger the operation and gets a different result.

You can also cache the outcome with Uni.memoize().indefinitely().

From Uni to CompletionStage

You can create a CompletionStage from Uni using uni.subscribeAsCompletionStage().

CompletionStage<String> cs = uni.subscribeAsCompletionStage();

It’s important to understand that retrieving a CompletionStage subscribes to the Uni. If you do this operation twice, it subscribes to the Uni twice and re-trigger the operation.

// Trigger the underlying operation twice:
CompletionStage<String> cs1 = uni.subscribeAsCompletionStage();
CompletionStage<String> cs2 = uni.subscribeAsCompletionStage();

Creating a Uni from a CompletionStage

To create a Uni from a CompletionStage, use Uni.createFrom().completionStage(…​).

Uni<String> uni1 = Uni
        // Create from a Completion Stage
        .createFrom().completionStage(
                CompletableFuture.supplyAsync(() -> "hello", executor)
        )
        .onItem().transform(String::toUpperCase);

Uni<String> uni2 = Uni
        // Create from a Completion Stage supplier (recommended)
        .createFrom().completionStage(
                () -> CompletableFuture.supplyAsync(() -> "hello", executor)
        )
        .onItem().transform(String::toUpperCase);

As you can see, there are two versions. The first one receives the CompletionStage directly, while the second one gets a supplier. In the case of multiple subscriptions on the produced Uni, the supplier is called multiple times (once per subscription), and so can change the return CompletionStage. It also delays the creation of the CompletionStage until there is a subscription, which only triggers the operation at that time. If you pass the instance directly, it will always use the same one (even for multiple subscriptions) and triggers the operation even if there is no subscription. For these reasons, it is generally better to use the variant accepting a supplier.

Note that if the completion stage produces a null value, the resulting Uni emits null as item. If the completion stages complete exceptionally, the failure is emitted by the resulting Uni.

Creating a Multi from a CompletionStage

To create a Multi from a CompletionStage, use Multi.createFrom().completionStage(…​). It produces:

  • a multi emitting an item and completing - if the value produced by the completion stage is not null,

  • an empty multi if the value produced by the completion stage is null,

  • a failed multi is completion stage is completed exceptionally.

Multi<String> multi1 = Multi
        .createFrom().completionStage(
                CompletableFuture.supplyAsync(() -> "hello", executor)
        )
        .onItem().transform(String::toUpperCase);

Multi<String> multi2 = Multi
        .createFrom().completionStage(() ->
                CompletableFuture.supplyAsync(() -> "hello", executor)
        )
        .onItem().transform(String::toUpperCase);

For the same reason as for Uni, there are two versions:

  1. one accepting a CompletionStage directly

  2. one accepting a Supplier<CompletionStage>, called at subscription-time, for every subscription.

It is recommended to use the second version.