DEV Community

Ava Parker
Ava Parker

Posted on

Unlock Lightning-Fast Web Services: Mastering Quarkus for Agile, Scalable, and Responsive RESTful APIs

This article explores the development of responsive RESTful APIs in Java, harnessing the power of Quarkus to create asynchronous endpoints that diverge from traditional synchronous approaches. To achieve this, Java developers must leverage the CompletableFuture and CompletionStage classes. This comprehensive guide provides an in-depth exploration of how to effectively utilize these classes, as well as how to chain asynchronous method invocations, including exception handling and timeouts.

Embracing the Paradigm Shift: The Case for Reactive REST APIs

The initial inquiry that arises is, why abandon established practices and opt for asynchronous code? After all, implementing reactive code can be uncharted territory for some Java developers, necessitating a fundamental shift in mindset.

In my opinion, the succinct answer lies in enhanced efficiency. I’ve conducted two load tests, comparing reactive code with imperative code. In both instances, the response times of the reactive code were significantly reduced, taking only half the duration of the imperative code. Although these tests are not representative of all scenarios, they aptly demonstrate the benefits of reactive programming.

For a detailed account of the performance tests, please refer to the documentation:

  • Simple REST API with Postgres database access
  • Cloud native application with multiple microservices

That being said, I don’t believe reactive REST APIs are a panacea. For instance, not every application requires high scalability. Furthermore, the development costs for reactive applications could be higher, as new skills may need to be acquired and traditional development processes may need to be adapted.

Constructing Your First Reactive REST API

The Quarkus guide Using Eclipse Vert.x provides a hello world example of a reactive REST API. To gain a deeper understanding of new technologies, I find it helpful to create simple sample applications after completing the getting started tutorials. This is why I’ve developed a sample application, available as part of the cloud-native-starter project.

The project encompasses a microservice dubbed ‘articles’, which furnishes a REST API that retrieves articles from a database. Let’s delve into the code:

Learn more about crafting agile web services with Quarkus

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import javax.ws.rs.core.Response;
import javax.json.JsonArray;
...
@GET@Path("/articles")@Produces(MediaType.APPLICATION_JSON)public CompletionStage<Response> getArticlesReactive(int amount) {
CompletableFuture<Response> future = new CompletableFuture<>();
articleService.getArticlesReactive(amount).thenApply(articles -> {
JsonArray jsonArray = articles.stream()
.map(article -> articleAsJson.createJson(article))
.collect(JsonCollectors.toJsonArray());
return jsonArray;
}).thenApply(jsonArray -> {
return Response.ok(jsonArray).build();
}).exceptionally(throwable -> {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}).whenComplete((response, throwable) -> {
future.complete(response);
});
return future;
}

Instead of returning a javax.ws.rs.core.Response object directly, a java.util.concurrent.CompletionStage containing a Response object is yielded. Notably, the CompletionStage is returned immediately, prior to the execution of the actual business logic. Once the business logic has been executed, the CompletionStage is completed (via ‘CompletionStage.complete’) and the response is delivered to the API client. The key benefit of this approach lies in the fact that the main thread remains unblocked while the business code is running.

The class java.util.concurrent.CompletableFuture serves as an implementation of the interface java.util.concurrent.CompletionStage. Furthermore, CompletableFuture also implements java.util.concurrent.Future, allowing code to await responses and read the responses via ‘CompletableFuture.get’. Additionally, CompletableFuture provides mechanisms for handling timeouts, which will be discussed in more detail below.

Mastering the capabilities and correct usage of CompletionStage and CompletableFuture can be a challenging task. The session CompletableFuture: The Promises of Java proved to be highly informative for me.

The actual business logic is provided by another class, called ‘ArticlesService’. The method ‘getArticlesReactive’ is an asynchronous method, returning a CompletionStage with a list of articles. Once the response is provided, methods like ‘CompletionStage.thenApply’, ‘CompletionStage.thenAccept’, and ‘CompletionStage.thenRun’ can be utilized to access the response.

All of these methods return a CompletionStage again, thereby enabling the methods to be chained, as exemplified above. ‘CompletionStage.thenApply’ allows receiving an input object and returning another object (wrapped in a completion stage). In the sample, the list of articles is converted into a JSON array, and subsequently, the array is converted into a Response.

Asynchronous Invocation Chains and Error Handling

The ‘articles’ microservice has been implemented with a clean architecture approach, where the code of the microservice is organized into three packages. These packages are relatively independent from each other and could be exchanged with other implementations.

  1. API: Contains the REST endpoints and handles incoming and outgoing messages.
  2. Business: Contains the business logic of the microservice and business entities.
  3. Data: Contains the code to access databases or other microservices.

In the sample, the REST endpoint from above resides in the API layer and invokes ArticlesService in the business layer.

Putting invocations into practice is a relatively simple process, as demonstrated earlier. Nevertheless, the true test of skill lies in effectively managing exceptions. The cloud-native-starter project also provides a synchronous implementation of the REST endpoint, enabling a comparison of error handling approaches in both scenarios.

The synchronous version of ArticlesService includes a method, ‘getArticles’, which throws two exceptions (refer to code).

List<Article> getArticles(int requestedAmount) throws NoDataAccess, InvalidInputParameter {

As usual these exceptions can be caught in the code that invokes the method (see code):

public Response getArticles(int amount) {
  JsonArray jsonArray;
  try {
    jsonArray = articleService.getArticles(amount).stream().map(article -> articleAsJson.createJson(article)).collect(JsonCollectors.toJsonArray());
    return Response.ok(jsonArray).build();
  } catch (NoDataAccess e) {
    return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
  } catch (InvalidInputParameter e) {
    return Response.status(Response.Status.NO_CONTENT).build();
  }
}

For asynchronous methods this mechanism obviously doesn’t work. The reactive version of ArticlesService that returns articles doesn’t declare exceptions in the interface (see code):

public CompletionStage<List<Article>> getArticlesReactive(int requestedAmount) {
  if (requestedAmount < 0)
    return CompletableFuture.failedFuture(new InvalidInputParameter());
  return dataAccess.getArticlesReactive();
}

To signal an exception, it returns a CompletableFuture and triggers the ‘failedFuture’ method with the actual exception as an argument.

When working with chained completion stages, exceptions can be intercepted via the ‘exceptionally’ method. These code paths will be executed when genuine exceptions have been thrown or when exceptions have been signaled via ‘CompletionStage.completeExceptionally’ (refer to code):

}).exceptionally(throwable -> {
  future.completeExceptionally(new NoConnectivity());
  return null;
});

It is clear that exception handling in imperative code takes a distinctly different approach compared to exception handling in asynchronous code with completion stages. Essentially, chained completion stages have two separate paths: the normal path and the exception path. If exceptions cannot be handled, they are propagated to the invoking code via ‘completeExceptionally’ and the exception path is executed. However, if exceptions can be handled, the flow can continue along the normal path. This is why the method ‘exceptionally’ in the previous snippet returns null. If the method could handle the exception, it could return an object to continue along the normal path.

The following snippet demonstrates how signaled exceptions can be caught. In this case, the REST endpoint implementation in the API layer handles the exceptions caused by ArticlesService in the business layer (refer to code):

articleService.getArticlesReactive(amount).thenApply(articles -> {
  ...
  return jsonArray;
}).thenApply(jsonArray -> {
  return Response.ok(jsonArray).build();
}).exceptionally(throwable -> {
  if (throwable.getCause().toString().equals(InvalidInputParameter.class.getName().toString())) {
    return Response.status(Response.Status.NO_CONTENT).build();
  }
  else {
    return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
  }
}).whenComplete((response, throwable) -> {
   future.complete(response);
});

Let me know if you need any further assistance!{"tool_calls":[ {"id": "call_lw9g4xehk7e3c2bxmt223ibv", "type": "function", "function": {"name": "h", "arguments": ">Mastering Asynchronous Timeouts

As demonstrated, exception handling in asynchronous code deviates from its synchronous counterpart. Another crucial pattern that Java developers must grasp when writing asynchronous code is the skill of managing timeouts.

When microservices successfully invoke asynchronous code, various CompletionStage methods, such as ‘thenApply’, are triggered upon completion. However, what if these completion stages never materialize? In such cases, the invoking code would be left in a state of perpetual limbo. A prime example of this scenario is microservices that interact with databases or invoke other services. In these instances, loading indicators in user interfaces would persist indefinitely if databases or services become unavailable.

Here’s an additional sample snippet where a Postgres database is accessed asynchronously:

public CompletableFuture<List<Article>> getArticlesReactive() {
  CompletableFuture<List<Article>> future = new CompletableFuture<List<Article>>();
  client.query("SELECT id, title, url, author, creationdate FROM articles ORDER BY id ASC")
    .toCompletableFuture()
    .orTimeout(MAXIMAL_DURATION, TimeUnit.MILLISECONDS).thenAccept(pgRowSet -> {
      List<Article> list = new ArrayList<>(pgRowSet.size());
      for (Row row : pgRowSet) {
        list.add(from(row));
      }
      future.complete(list);
    }).exceptionally(throwable -> {
    future.completeExceptionally(new NoConnectivity());
      return null;
    });
    return future;
}

To efficiently handle timeouts, the 'CompletableFuture.orTimeout' method can be utilized. When the execution time surpasses the allotted timeframe, the code within the 'exceptionally' block is triggered. Note that this method is exclusively available in Java 9 and later versions.

It's worth noting that 'orTimeout' is a method of CompletableFuture, not CompletionStage. Fortunately, you can seamlessly convert completion stages to completable futures using the 'CompletionStage.toCompletableFuture' method.

Next Steps

This article is part of a comprehensive series. To delve deeper into reactive programming, read the other articles in this series:

  • Development of Reactive Applications with Quarkus
  • Accessing Apache Kafka from Quarkus
  • Accessing PostgreSQL in Kubernetes from Quarkus
  • Reactive Messaging Examples for Quarkus
  • Invoking REST APIs asynchronously with Quarkus
  • Comparing synchronous and asynchronous Access to Postgres
  • More will be added here soon ….

All samples from this article are included in the open-source project cloud-native-starter. Explore it to see the code in action.

The project encompasses not only the articles service but also a comprehensive cloud-native application comprising multiple microservices, Postgres, and Kafka:

Top comments (0)