Vert.x 4.x Async Composition: Default to Future/Promise, Reserve RxJava3 and Mutiny
Decision guide for Vert.x 4.x services: default to built-in Future/Promise for async composition, reserving RxJava3 or Mutiny for backpressure needs. Includes implementation patterns and validation.
13 Oct 2025, 10:56 UTC

Decision and Constraints
When building new Vert.x 4.x services, selecting an asynchronous composition model is a critical architectural decision. The recommended approach is to adopt Vert.x Future and Promise as the default for new services, reserving RxJava3 or Mutiny only when reactive streams backpressure or a rich operator library is a hard requirement.
This decision is driven by several constraints: team Java familiarity, observability of stack traces, dependency weight, and migration cost. Future/Promise is built into vertx-core, aligns natively with all Vert.x APIs, and keeps the dependency graph minimal. While RxJava3 and Mutiny provide powerful streaming capabilities, they introduce a second reactive model that increases cognitive load and onboarding time.
Options Comparison
| Model | Dependency | Backpressure | Operators | Integration | Debugging |
|---|---|---|---|---|---|
| Future/Promise | Built-in (vertx-core) | No | compose, map, flatMap, recover | Native to Vert.x APIs | Straight stack traces |
| RxJava3 | vertx-rx-java3 | Yes (Reactive Streams) | Full RxJava3 set | Interop adapters | Deeper stacks, leak risk |
| Mutiny | vertx-mutiny | Yes (Reactive Streams) | Uni/Multi fluent API | Interop adapters | Readable chains |
Trade-offs and Risks
Future/Promise minimizes overhead and aligns with core Vert.x patterns, making it the easiest path for developers familiar with standard Java. The primary limitation is the lack of backpressure—the ability for a consumer to signal to a producer that it is overwhelmed. For simple request-response patterns, this is rarely an issue, but it becomes a bottleneck for unbounded data streams.
RxJava3 maximizes expressiveness for complex stream processing but increases the risk of subscription leaks if not managed carefully. Because Vert.x 4.x dropped RxJava2, RxJava3 must be added as an explicit dependency. Mutiny offers a more modern, readable API via Uni and Multi, but like RxJava3, it requires the team to learn a distinct reactive paradigm alongside the Vert.x event loop model.
Critical Warning: Never block the event loop. Avoid calling Future.get() or performing blocking I/O within a handler. Use vertx.executeBlocking() to offload CPU-intensive tasks to a worker pool.
Concrete Implementation Pattern
The following pattern demonstrates how to create a Promise, offload a CPU-intensive task using executeBlocking, and chain a reactive SQL client call using compose.
public Future<ResultSet> fetchAndProcess(String id, Vertx vertx, SqlClient client) {
Promise<ResultSet> promise = Promise.promise();
vertx.executeBlocking(promising -> {
// CPU-intensive work performed off the event loop
String prepared = prepareQuery(id);
promising.complete(prepared);
}).compose(prepared -> client.preparedQuery(prepared).execute())
.onSuccess(promise::complete)
.onFailure(promise::fail);
return promise.future();
}
In this example, executeBlocking ensures the event loop remains responsive. The compose method is used to chain the result of the blocking operation into a subsequent asynchronous SQL call without nesting callbacks.
Validation and Verification
To validate that the composition works without blocking the event loop, use Vert.x Unit tests. Ensure the test asserts both success and failure paths without calling blocking methods.
vertx.runTest(testContext -> {
Future<String> f = Future.succeededFuture(1).map(i -> "Result: " + (i + 1));
f.onSuccess(v -> testContext.verify(() -> {
testContext.assertEquals("Result: 2", v);
testContext.complete();
}));
f.onFailure(err -> testContext.failNow(err));
});
Verification steps include checking the Maven coordinates for vertx-rx-java3 or vertx-mutiny in the official Vert.x 4.x module list before adding them. Review the Future API to ensure compose, map, and recover signatures match your implementation.
Limitations and Practical Checks
If your service must handle high-volume, unbounded streams from external sources, Future/Promise will not suffice due to the lack of backpressure. In such cases, evaluate Mutiny Multi or RxJava3 Flowable.
To check for event loop blocking in a live environment, monitor the Vert.x blocked thread checker logs. Any warning indicating a handler was blocked for more than a few seconds suggests that a blocking call was not properly wrapped in executeBlocking.
To maintain maintainability, avoid mixing these models within a single module. Use interop adapters only at the boundaries of different architectural layers.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.