Using Vert.x Event Bus Request‑Response for Loosely Coupled Services
Learn how Vert.x’s request‑response pattern simplifies service communication, see a runnable example, and understand the trade‑offs of timeout handling and clustering.
10 Aug 2025, 23:25 UTC

Problem: Coupled service calls make scaling painful
When building reactive microservices, direct HTTP or shared‑memory calls often tie services together. Changing a service’s location or adding instances requires updating client code, and handling failures manually leads to boilerplate.
Thesis: Vert.x request‑response gives location‑independent, at‑most‑once messaging with built‑in timeout handling
The Vert.x Event Bus provides a request‑response pattern that creates a temporary reply address, automatically cleans it up, and delivers either a reply or a failure. When the bus is clustered, the same code works across nodes without extra discovery logic.
How the pattern works
- Request:
eventBus.send(address, message, options, handler)creates a unique temporary reply address. - Response: The handler registered on
addressreceives the message, processes it, and callsmessage.reply(result). - Cleanup: After a successful reply or after the timeout expires, the temporary address is removed, preventing leaks.
Worked example: a simple ping‑pong service
Below are two verticles: a PingSender that sends a request every second, and a PongReceiver that replies with "pong". The example uses Vert.x Core 4.5.0 (or later).
// pom.xml snippet
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<version>4.5.0</version>
</dependency>
// PingSender.java
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Vertx;
import io.vertx.core.eventbus.Message;
public class PingSender extends AbstractVerticle {
@Override
public void start() {
vertx.setPeriodic(1000, id -> {
vertx.eventBus().request("pong-address", "ping", ar -> {
if (ar.succeeded()) {
Message reply = ar.result();
System.out.println("Received: " + reply.body());
} else {
System.out.println("Request failed: " + ar.cause());
}
});
});
}
}
// PongReceiver.java
import io.vertx.core.AbstractVerticle;
public class PongReceiver extends AbstractVerticle {
@Override
public void start() {
vertx.eventBus().consumer("pong-address", msg -> {
System.out.println("Received ping, replying pong");
msg.reply("pong");
});
}
}
To run:
- Start Vert.x with the sender:
vertx run PingSender.java - In another terminal, start the receiver:
vertx run PongReceiver.java - Observe the console: each second the sender logs "Received: pong".
Clustering for location‑independent communication
By default the event bus is non‑clustered and limited to a single JVM. Adding a cluster manager (e.g., Hazelcast) makes the bus transparent across nodes.
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-hazelcast</artifactId>
<version>4.5.0</version>
</dependency>
Create a clustered Vertx instance:
VertxOptions options = new VertxOptions().setClustered(true);
Vertx.clusteredVertx(options, res -> {
if (res.succeeded()) {
Vertx vertx = res.result();
// deploy verticles as usual
} else {
System.err.println("Cluster failed: " + res.cause());
}
});
Deploy two nodes on different ports (e.g., via -Dport=8080 and -Dport=8081). A request sent from node 1 to "pong-address" will be handled by any node that has registered a consumer, without the sender needing to know the target’s address.
Trade‑offs and limitations
- Timeout handling: The
requestmethod accepts aDeliveryOptionswith asetTimeoutvalue. If the receiver does not reply within that period, the sender’s handler receives a failure. Setting the timeout too low under load can cause cascading failures, while a too‑high timeout delays error detection. - Clustering overhead: Enabling a cluster manager introduces network traffic for distributing the event bus topology and serializing messages. For intra‑VM calls the overhead is negligible, but cross‑node hops add latency (typically sub‑millisecond to a few milliseconds depending on network).
- At‑most‑once guarantee: Vert.x guarantees at‑most‑once delivery per request when using
sendwith a handler. If exactly‑once semantics are required, the application must implement idempotency or duplicate detection.
Actionable closing
Start with the non‑clustered example to verify that request‑response works locally. Then add a cluster manager and run two nodes to see location‑independent behavior in action. Adjust the timeout based on your service’s expected latency and monitor failure rates to avoid premature cascades. This pattern gives you a clean, reactive way to decouple services while keeping failure handling straightforward.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.