Quarkus RestClient vs Spring WebFlux: performance comparison for consume asynchronous REST APIs.
Introduction
In previous article (Quarkus RestClient vs Quarkus Vert.x: performance comparison for asynchronous tasks), we exposed two Quarkus APIs that consume just one external REST API. Then, we test them using JMeter for 100, 200 and 300 requests.
In this article, we will expose a Quarkus and Spring APIs where each request will consume three REST APIs. Both APIs will be tested using JMeter and we will review how many concurrent requests could support in one second without failures.
This could be a real-world use case, because we can expose an API which internally will consume N external APIs. Then, our API should return an unique success or failure response after consume/wait for all of them.
Architecture
First, we are going to create two Quarkus projects.
Factorial External Api, is a quarkus project with a Get Rest API.
- /getFactorialValue: return the factorial value for its input parameter.
Factorial Reactive, is a quarkus project with a Get Rest API that will be tested using JMeter.
- /getFactorialByUniCombine: Use a Quarkus RestClient implementation to consume three external APIs and combine them after receive all responses using Quarkus <Uni>.
The previous picture shows the architecture diagram used to test the performance for N requests in one second using Apache JMeter. We are going to test for 50, 60 and 70 requests.
For our Quarkus tests projects we are going to use the Quarkus version 3.5+.
Factorial WebFlux, is a SpringBoot project with a Get Rest API.
- /getFactorialByWebFlux: Use the Spring Web Client implementation to consume three external APIs and merge them after receive all responses using Spring WebFlux.
For our SpringWebFlux project, we are going to create a SpringBoot test project with version 3.2.0.
Now, let’s delve into the specifics of the application code for each project.
Factorial External API
We are going to reuse this Quarkus project created in a previous post. We can review the Java code here.
For this demo, we are including a sleep time between 1000 or 2000 MS for some of these requests.
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: Rest Client Reactive, RESTEasy Reactive
You can also see Java code here.
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 java class created that expose the Get API to be tested from JMeter. This API will consume three times the Get API from ‘factorial-external-api’ project.
package org.acme;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.smallrye.mutiny.Uni;
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);
@Inject
@RestClient
FactorialService factorialService;
@GET
@Path("/combineUniResponse")
public Uni<Object> getFactorialByUniCombine() {
var idProcess = UUID.randomUUID();
int NUMBER_EXTERNAL_APIS = 3;
List<Uni<Response>> results = new ArrayList<>();
for(int i=1;i<=NUMBER_EXTERNAL_APIS;i++) {
int valueToCalculate = getRandomNumber(2,10);
LOGGER.info("combineUniResponse, START idProcess [{}] with valueToCalculate [{}]", idProcess, valueToCalculate);
results.add(factorialService.getFactorialValueFromClient(String.valueOf(valueToCalculate), String.valueOf(idProcess)));
}
// collect all the Uni results
Uni<Object> combined = null;
try {
combined = Uni.combine().all().unis(results).with(ls -> {
List<String> resp = new ArrayList<String>();
for(Object obj:ls) {
resp.add(((Response)obj).readEntity(String.class));
}
return resp;
});
LOGGER.info("combineUniResponse, END idProcess [{}]", idProcess);
} catch (Exception e) {
LOGGER.error("Exception error", e);
combined = Uni.createFrom().failure(e);
}
return combined;
}
private int getRandomNumber(int min, int max) {
return (int) ((Math.random() * (max - min)) + min);
}
}
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
After hit a request for previous API, we can see this API will consume three times the ‘Factorial external API’ with some random values and print the results. In next section, we will stress this API with 50, 60 and 70 request in one second. So, we can compare the results with Spring WebFlux.
Factorial WebFlux
Create a new SpringBoot project using Spring Initializer.
- Group: org.acme
- Artifact: factorial-webflux
- Dependencies: SpringBoot starter web, SpringBoot starter webflux
Import maven project in Eclipse IDE and add the following classes.
Pom.xml file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>org.acme</groupId>
<artifactId>factorial-webflux</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>factorial-webflux</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
You can also see Java code here.
Java code
Factorial, is a Java Bean used to build the API response.
package org.acme.factorialwebflux;
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;
}
}
FactorialClientController, is a java class created to expose the API to be tested from JMeter.
package org.acme.factorialwebflux;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/factorialWebFlux")
public class FactorialClientController {
@Autowired
FactorialClientService servicio;
@RequestMapping("/list")
public List<Factorial> getFactorialByWebFlux() {
List<Factorial> lista = servicio.getFactorials().collectList().block();
return lista;
}
}
FactorialClientService, is a java class created to consume the ‘Factorial External API’. This API will be consumed three times with random values.
package org.acme.factorialwebflux;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
@Service
public class FactorialClientService {
Logger LOGGER = LoggerFactory.getLogger(FactorialClientService.class);
public Flux<Factorial> getFactorials() {
var idProcess = UUID.randomUUID();
WebClient cliente = WebClient.create("http://localhost:8080/factorialExternalApi");
int NUMBER_EXTERNAL_APIS = 3;
List<Flux<Factorial>> results = new ArrayList<>();
for(int i=1;i<=NUMBER_EXTERNAL_APIS;i++) {
int valueToCalculate = getRandomNumber(2,10);
LOGGER.info("getFactorials, START idProcess [{}] with valueToCalculate [{}]", idProcess, valueToCalculate);
results.add(cliente.get().uri("/calculate/"+valueToCalculate+"/uuid/"+idProcess).retrieve().bodyToFlux(Factorial.class));
}
Flux<Factorial> webFluxResult=Flux.merge(results);
LOGGER.info("getFactorials, END idProcess [{}]", idProcess);
return webFluxResult;
}
private int getRandomNumber(int min, int max) {
return (int) ((Math.random() * (max - min)) + min);
}
}
FactorialWebfluxApplication, is a Java class already generated to start up the Sprig Boot project and looks like.
package org.acme.factorialwebflux;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class FactorialWebfluxApplication {
public static void main(String[] args) {
SpringApplication.run(FactorialWebfluxApplication.class, args);
}
}
application.properties
- Let’s configure the port where to run the application.
server.port=8082
Run application
Configure Maven Build from eclipse and Run SpringBoot application.
Let’s keep the ‘factorial-external-api’ project running too.
Then, test API from web browser with the following request.
After hit a request for previous API, we can see this API will consume three times the ‘Factorial external API’ with some random values and print the results. The same expected result as ‘Quarkus Factorial Reactive’ project described before. Now, we are ready to stress both APIs with JMeter and compare the results.
Testing from JMeter
Let’s use JMeter to send 50, 60 and 70 requests around one second for both APIs and compare results. The following diagrams just show the results for 70 requests and we will show the results summary for 50 and 60 requests in the next section.
Factorial Quarkus
After run the application JMeter will send 70 requests in around one second. Furthermore, we are seeing many timeout error messages for many requests.
1:58:58,879 ERROR [io.qua.ver.htt.run.QuarkusErrorHandler] (vert.x-eventloop-thread-1) HTTP Request to /factorialApi/combineUniResponse failed, error id: 7ed202c4–2693–4a40-bebb-6ea03eca5d6d-28: 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 15 error requests in JMeter.
Now, let’s test using the Spring API.
Factorial WebFlux
After run the application JMeter will send 70 requests in around one second. The console log shows all requests were processed successfully.
JMeter shows all 70 requests were processed successfully.
PERFORMANCE COMPARISON
After sending 50, 60 and 70 requests from JMeter 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.
From the chart, we can establish:
- For 50 requests, Spring WebFlux has a better response time than Quarkus. However, for both APIs all requests were successful.
- For 60 requests, there are approximately 10 failures for Quarkus requests. Meanwhile, Spring WebFlux process all of them successfully.
- For 70 requests, Quarkus fails more than 15 requests. However, Spring WebFlux process all of them successfully again.
Increase requests number for Spring WebFlux
Previously, we have tested Spring WebFlux with 70 requests in around one second. Let’s increase this number to 80, 90 and 100 using JMeter. Let’s see the results in the following chart.
As we can see, Spring WebFlux supports up to 100 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 Spring WebFlux is a great choice if your application expects to receive concurrent requests and each request will consume N external Rest APIs. For this demo, we have tested with three external Rest APIs.
Next steps
- Testing and comparing the results between Vertx (Native) and Spring Webflux to process N external Rest APIs.
- Increase the number of external Rest APIs to consume per request. For our current demo we have been testing with three.
- Testing and comparing the results using other load test tool like Bombardier.
- Understand the concepts how Spring WebFlux works internally. So, we can be ready to compare with Vertx.