Quarkus RestClient vs Quarkus Vert.x: performance comparison for asynchronous tasks.

Michael Felipe Ayala
GlobalLogic LatAm
Published in
10 min readDec 1, 2023

Reactive programming is based on the idea of asynchronous event processing. Asynchronous processing means that the processing of an event does not block the processing of other events. In response to popular demand, this article delves into the comparison between Quarkus Mutiny and Quarkus Vert.x. However, let’s give a brief overview about Quarkus and why is so famous today.

Quarkus was created to enable Java developers to create applications for a modern, cloud-native world. Quarkus is designed to seamlessly combine the familiar imperative style code and the non-blocking, reactive style when developing applications. If your endpoint method needs to accomplish an asynchronous or reactive task before being able to answer, you can declare your method to return the Uni type from Mutiny.

For our Quarkus tests projects we are going to use the Quarkus version 3.5+.

Quarkus Mutiny is an intuitive, reactive programming library. It is the primary model to write reactive applications with Quarkus. Mutiny is very different from the other reactive programming libraries. With Mutiny everything is event-driven: you receive events, and you react to them.

Mutiny offers two types that are both event-driven and lazy:

  • A Uni emits a single event (an item or a failure). Unis are convenient to represent asynchronous actions that return 0 or 1 result. A good example is the result of sending a message to a message broker queue.
  • A Multi emits multiple events (n items, 1 failure or 1 completion). Multis can represent streams of items, potentially unbounded. A good example is receiving messages from a message broker queue.

On this article we will be exposing reactive APIs using Uni type.

Using the following maven dependency we can start working with Quarkus Mutiny.

    <dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>

From the other side, Eclipse Vert.x is a toolkit used for building reactive applications on the JVM using an asynchronous and non-blocking execution model. Vert.x han­dle more re­quests with less re­sources com­pared to tra­di­tional stacks and frame­works based on block­ing I/O. Also, is a great fit for all kinds of ex­e­cu­tion en­vi­ron­ments, in­clud­ing con­strained en­vi­ron­ments like vir­tual ma­chines and con­tain­ers.

Quarkus uses Vert.x underneath, for this reason Quarkus applications can access and use the Vert.x APIs.

Using the following maven dependency we can start working with Quarkus Vertx.

    <dependency>
<groupId>io.smallrye.reactive</groupId>
<artifactId>smallrye-mutiny-vertx-web-client</artifactId>
</dependency>

Getting Started

First of all, make you have the following software installed.

  • JDK 21
  • Apache Maven 3.9+
  • Eclipse 4.29+
  • JMeter 5.5

Remember to setup JAVA_HOME and M2_HOME before to start.

To assess Quarkus performance, a series of rigorous tests were conducted on a Windows 11 Core i7 with 40GB of RAM. The load testing tool employed for these evaluations was JMeter, a reliable choice for measuring application performance under varying loads. The tests were carried out using Java 21, ensuring the most recent improvements and optimizations were accounted for in the assessment.

Architecture

As part of this performance comparison, we’re going to create two Quarkus projects.

Factorial External Api, is a quarkus project with just one Get Rest API.

  • /getFactorialValue: return the factorial value for its input.

Factorial Reactive, is a quarkus project with two Get Rest APIs.

  • /getFactorialUsingVertxWebClient: Use a vertx implementation to consume an external API.
  • /getFactorialUsingRestClient: Use a RestClient implementation to consume an external API.
Architecture diagram to test reactive APIs.

The previous picture shows the architecture diagram used to test the performance for 300 requests in one second using Apache JMeter. Then, we will test for 100 and 200 requests too.

Now, let’s delve into the specifics of the application code for each project.

Factorial External API

First, we need to create a new project. Let’s run the following maven command:

mvn io.quarkus.platform:quarkus-maven-plugin:3.5.1:create 
-DprojectGroupId=org.acme
-DprojectArtifactId=factorial-external-api
-Dextensions=resteasy-reactive-jsonb
  • Group: org.acme
  • Artifact: factorial-external-api
  • Extensions: RESTEasy Reactive Jsonb

Currently, we can’t create a Quarkus project from Quarkus Configure Application (https://code.quarkus.io/) because it doesn’t support Java 21 yet.

Import maven project in Eclipse IDE and add the following classes.

Java code

Factorial, is a Java Bean used to build the API response.

package org.acme;

public class Factorial {
private int input;
private long output;
private String timeElapsed;
private String uuid;

public Factorial(){

}

public Factorial(int input, long output, String timeElapsed, String uuid) {
this.input = input;
this.output = output;
this.timeElapsed = timeElapsed;
this.uuid = uuid;
}

public int getInput() {
return input;
}
public long getOutput() {
return output;
}
public String getTimeElapsed() {
return timeElapsed;
}
public String getUuid() {
return uuid;
}
}

FactorialService, is a service class used to calculate the factorial value and return an Uni response. Furthermore, the code generates an internal random number to sleep the current Thread for some Milliseconds. This sleep time helps us to simulate a slow or fast request.

package org.acme;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.core.Response;

@ApplicationScoped
public class FactorialService {

Logger LOGGER = LoggerFactory.getLogger(FactorialService.class);

public Uni<Response> getFactorialValue(int input, String uuid) {
long startTime = System.currentTimeMillis();
long output = calculateFactorialValue(input);

// GENERATE A RANDOM NUMBER TO SELECT SLEEP TIME
int sleepTime = 0;
if((getRandomNumber(1,10)%2)==0){
sleepTime = getRandomNumber(1,3) * 1000; // Sleep <1000,3000> MS for SLOW Request
} else {
sleepTime = 10; // Sleep 10 MS for FAST Request
}

try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
LOGGER.error("Error while Thread sleep process for uuid [{}] ", uuid,e);
Response errorResponse = Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Error while Thread sleep process for uuid "+uuid).build();
return Uni.createFrom().item(errorResponse);
}

long endTime = System.currentTimeMillis();
long timeElapsed = endTime - startTime;

Factorial factorial = new Factorial(input, output, timeElapsed+" MS", uuid);
Response response = Response.ok(factorial).build();
LOGGER.info("END PROCESS, input [{}] output [{}] timeElapsed [{} MS] uuid [{}]", input, output, timeElapsed, uuid);

return Uni.createFrom().item(response);
}

private long calculateFactorialValue(int n) {
long fact = 1;
for (int i = 2; i <= n; i++) {
fact = fact * i;
}
return fact;
}

private int getRandomNumber(int min, int max) {
return (int) ((Math.random() * (max - min)) + min);
}
}

FactorialExternalAPI, this class exposes the Get Uni Response API to consume.

package org.acme;

import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

@Path("/factorialExternalApi")
@Produces(MediaType.APPLICATION_JSON)
public class FactorialExternalAPI {

@Inject
FactorialService factorialService;

@GET
@Path("/calculate/{input}/uuid/{uuid}")
public Uni<Response> getFactorialValue(@PathParam("input") int input, @PathParam("uuid") String uuid){
return factorialService.getFactorialValue(input, uuid);
}
}

Run application

Configure Maven Build from eclipse and Run application.

Configure Maven Build for ‘factorial-external-api’ project

Also, you can use maven. Navigate into project directory and launch your application with maven command.

mvn compile quarkus:dev

Now, we can test the API from web browser

Testing Factorial External API from web browser

Factorial Reactive

Create a new project with the following maven command:

mvn io.quarkus.platform:quarkus-maven-plugin:3.5.1:create 
-DprojectGroupId=org.acme
-DprojectArtifactId=factorial-reactive
-Dextensions=rest-client-reactive,resteasy-reactive
  • Group: org.acme
  • Artifact: factorial-reactive
  • Extensions: Smallrye Mutiny Vertx Web Client, Rest Client Reactive, RESTEasy Reactive

Now, let’s add the following maven dependency in pom.xml file.

<dependency>
<groupId>io.smallrye.reactive</groupId>
<artifactId>smallrye-mutiny-vertx-web-client</artifactId>
</dependency>

Import maven project in Eclipse IDE and add the following classes.

Java code

FactorialService, Interface created to consume ‘factorial-external-api’ project. REST Client Reactive is as simple as creating an interface using the proper Jakarta REST and MicroProfile annotations.

package org.acme;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

@RegisterRestClient(configKey = "factorial-external-api")
@Produces(MediaType.APPLICATION_JSON)
public interface FactorialService {

@GET
@Path("/calculate/{input}/uuid/{uuid}")
Uni<Response> getFactorialValueFromClient(@PathParam("input") String input, @PathParam("uuid") String uuid);
}

FactorialReactive, is a class created that expose the two Get APIs. Both APIs will consume the Get API from ‘factorial-external-api’ project.

package org.acme;

import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;

import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.client.WebClientOptions;
import io.vertx.mutiny.core.Vertx;
import io.vertx.mutiny.ext.web.client.WebClient;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;

@Path("/factorialApi")
public class FactorialReactive {

Logger LOGGER = LoggerFactory.getLogger(FactorialReactive.class);

private final WebClient client;
private static AtomicInteger atomicThreadCounter = new AtomicInteger();

@Inject
@RestClient
FactorialService factorialService;

public FactorialReactive(Vertx vertx) {
this.client = WebClient.create(vertx,
new WebClientOptions().setDefaultHost("localhost").setDefaultPort(8080).setSsl(false)
.setTrustAll(true));
}

@GET
@Path("/vertxWebClient")
public Uni<Response> getFactorialUsingVerxtWebClient() {
var idProcess = UUID.randomUUID();
LOGGER.info("VERTX, START idProcess [{}] with ATOMIC_VALUE [{}]", idProcess, atomicThreadCounter.getAndIncrement());

// GENERATE A RANDOM NUMBER BETWEEN 2 TO 10
int valueToCalculate = getRandomNumber(2,10);

return client.get("/factorialExternalApi/calculate/" + valueToCalculate+"/uuid/"+idProcess)
.send()
.onFailure()
.invoke(ex -> LOGGER.error("Couldn't execute EXTERNAL Factorial API", ex))
.map(resp -> {
if (resp.statusCode() == 200) {
LOGGER.info("VERTX, END idProcess [{}] with ATOMIC_VALUE [{}]", idProcess, atomicThreadCounter.getAndDecrement());
return Response.ok(resp.bodyAsJsonObject()).build();
} else {
String errorMessage = "Error connecting with factorial external API";
LOGGER.error(errorMessage);
return Response.status(resp.statusCode()).entity(errorMessage).build();
}
});
}

@GET
@Path("/restClient")
public Uni<Response> getFactorialUsingRestClient() {
var idProcess = UUID.randomUUID();
try {
LOGGER.info("UNI, START idProcess [{}] with ATOMIC_VALUE [{}]", idProcess, atomicThreadCounter.getAndIncrement());
int valueToCalculate = getRandomNumber(2,10);
Uni<Response> resp = factorialService.getFactorialValueFromClient(String.valueOf(valueToCalculate), String.valueOf(idProcess));
return resp;
} finally {
LOGGER.info("UNI, END idProcess [{}] with ATOMIC_VALUE [{}]", idProcess, atomicThreadCounter.getAndDecrement());
}
}

private int getRandomNumber(int min, int max) {
return (int) ((Math.random() * (max - min)) + min);
}

}

The two GET API methods have an atomic variable named ‘atomicThreadCounter’. This variable is used to demonstrate the APIs are receiving concurrent requests around one second.

application.properties

  • Let’s configure the port where to run the application. It should be different from the previous project.
  • Configure the client for ‘factorial-external-api’ project with the correct URL and port.
quarkus.http.port=8083
# Factorial External API Client
factorial-external-api/mp-rest/url=http://localhost:8080/factorialExternalApi

Run application

Configure Maven Build from eclipse and Run application.

Configure Maven Build for ‘factorial-reactive’ project.

Let’s keep the ‘factorial-external-api’ project running too.

Then, test both APIs from web browser with the following requests.

Testing Vertx implementation from web browser
Testing Rest Client implementation from web browser

Now we are ready to test both APIs using JMeter.

Testing from JMeter

Let’s use JMeter to send 100, 200 and 300 requests around one second for both APIs and compare results. The following diagrams just show the results for 300 requests and we will show the results summary for 100 and 200 requests in the next section.

FactorialVertxWebClient

Thread Group to load testing for Vertx implementation
HTTP Request for Vertx API implementation.

After run the application JMetter will send 300 requests in around one second. The console log shows the atomic variable increase until reach 300 requests, then reduce them until 1.

Atomic variable increase until 300 and start to decrease
Atomic variable decrease until 1

JMetter shows all 300 requests were processed successfully.

Result Tree for Vertx implementation

Now, let’s test using the Quarkus RestClient API.

FactorialUniRestClient

Thread Group to load testing for RestClient implementation
HTTP Request for Rest Client implementation

After run the application JMetter will send 300 requests around one second. The console log shows the atomic variable increases its value to 1 and reduce to 0 for each request.

We can conclude that the API is processing all requests around one second in asynchronous mode. It doesn’t wait to complete the task to reply a response.

Atomic variable increase a decrease.

Furthermore, we are seeing many timeout error messages for many requests.

17:29:32,161 ERROR [io.qua.ver.htt.run.QuarkusErrorHandler] (vert.x-eventloop-thread-1) HTTP Request to /factorialApi/restClient failed, error id: e2497af0–43a1–4e46–98a7–139b3d506f0f-1: io.vertx.core.http.impl.NoStackTraceTimeoutException: The timeout of 30000 ms has been exceeded when getting a connection to localhost:8080

There are more than 150 error requests in JMetter.

Result Tree for RestClient implementation
Result Tree for RestClient implementation

PERFORMANCE COMPARISON

After sending 100, 200 and 300 requests from JMetter where each group of requests will be sent in a period of one second. The results will be summarized in the next chart. It shows the time taken (hh:mm:ss:SSS) and number of success/failure requests for each group or requests.

Performance comparison after consume N requests using Vertx and RestClient

From the chart, we can establish:

  • For 100 requests, Vertx and RestClient (Uni) got almost a similar response time and all of them were successful.
  • For 200 requests, there are approximately 72 failure for RestClient requests. Meanwhile, Vertx process all of them successfully.
  • For 300 requests, RestClient fails more than the middle of their requests. However, Vertx process all of them successfully again.

Increase requests number for Vertx

Previously, we have tested Vertx with 300 requests in around one second. Let’s increase this number to 500, 750 and 1000 using JMetter. Let’s see the results in the following chart.

As we can see, Vertx supports up to 1000 requests in one second without any failure. Also, the memory consumption remains stable for all these numbers of requests. Seeing the results, we can establish that Vertx is a great choice if your application expects to receive concurrent requests and avoid failure requests.

Next steps

Thanks for reading!

--

--