Our Journey with Quarkus

6 min read

Quarkus truly appears to be the smartest possible solution, in our experience.

The Java platform is an industry leader when it comes to portability and extensibility. It is also known for its support, maintainability and a rich set of libraries. With its fantastic features, Java is considered by many to be the premier enterprise programming language in the world. 

As the software industry migrated to the cloud, the superiority of Java was questioned. Deploying fat war files to application servers is not fun. Fortunately, frameworks like Spring Boot came to the rescue. But still, even Spring Boot applications consume a lot of resources and take a long time to start up, important limitations for technologies like container orchestration and serverless

The question now is — is it possible to write slim, light, fast boot time, low-memory footprint Java applications? The answer is a definitive yes, and the solution is Quarkus.

Quarkus is a full-stack, cloud-native Java framework developed by Red Hat. It supports Java Virtual Machine (JVM) as well as native compilation. Quarkus is based on MicroProfile standard and some Jakarta EE standards. It has faster startup times and requires less memory. Additionally, from the beginning, Quarkus was designed to be a container-first framework. Thus, it makes Java an effective platform for serverless applications, cloud and Kubernetes environments.

Quick recap of this blog series

The last blog post in this series detailed how we started our StoreFront application's cloud-native journey to Red Hat OpenShift. If you have not checked our blog series introduction yet, be sure to see "Our Cloud Native Journey to Red Hat OpenShift Using Quarkus." 

In this blog post, I will discuss how we implemented the StoreFront application's Java microservices using Quarkus. In case you are joining late, this blog series details my team's experience building the StoreFront application microservices using Quarkus and deploying them to Red Hat Openshift using a GitOps framework:

In this blog post, I will discuss how we implemented the StoreFront application's Java microservices using Quarkus.

If interested, you can find additional information about the StoreFront application and Java microservices architectures in the following resources:

You can also browse the Quarkus guides to become a Quarkus expert.

Quarkus features for our microservices

In the StoreFront application's Java microservices, we implemented RESTful APIs, Configuration, Service Invocation, Resilience, Security and Monitoring using Quarkus:

In the StoreFront application's Java microservices, we implemented RESTful APIs, Configuration, Service Invocation, Resilience, Security and Monitoring using Quarkus:

In this blog post, we will explore the following features for two microservices of the StoreFront application:

  • RESTful APIs: JAX-RS, CDI, JSON-B, JSON-P, Open API
  • Configuration: Config
  • Service Invocation: Rest Client
  • Resilience: Fault Tolerance, Health
  • Monitoring: Metrics, Open Tracing

Build StoreFront microservices using Quarkus

Let us consider the Inventory microservice and the Catalog microservice of the StoreFront application. The Inventory microservice returns a list of all the antique computing devices that are available. This microservice uses a MySQL database as its datasource. The Catalog microservice uses Elasticsearch and serves as cache to the Inventory microservice:

The Catalog microservice uses Elasticsearch and serves as cache to the Inventory microservice:

RESTful APIs

Take a look at the APIs defined for the Catalog microservice:

Take a look at the APIs defined for the Catalog microservice:
import java.io.IOException;
import java.util.List;
 
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
 
import ibm.cn.application.model.Item;
import ibm.cn.application.repository.ItemService;
 
@Path("/micro/items")
public class CatalogResource {
 
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Item> getInventory() throws IOException {
         // code
    }
     
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("{id}")
    public Response getById(@PathParam("id") long id) throws IOException {
        // code
    }
}
  • JAX-RS can be used in Quarkus by simply annotating resources with the @Path annotation. @path("/micro/items") specifies the base path in the URL. Requests for this service will be routed via this base path.
  • GET /micro/items uses the GET HTTP method to return the list of items available in the inventory. 
  • GET /micro/items/{id} uses the GET HTTP method to return the item available in the inventory based upon the item id.
  • @Produces automates the serialization of JSON. The JSON output produced by your services is generated automatically by analyzing the objects returned by your services. It sets the Content-Type header on the response to application/json.

Configuration

To configure a Quarkus application, we can use the application.properties file as the configuration source.

The application.properties file for the Catalog microservice contains the following values:

quarkus.elasticsearch.hosts = http://${ELASTICSEARCH_HOST:localhost}:${ELASTICSEARCH_PORT:9200}

elasticsearch.index=micro
elasticsearch.doc_type=items

And here is the Java code that reads the configuration and assigns it:

import org.eclipse.microprofile.config.ConfigProvider;

public class ElasticSearchDataLoad {

    private String url = ConfigProvider.getConfig().getValue("quarkus.elasticsearch.hosts", String.class);
    private String index = ConfigProvider.getConfig().getValue("elasticsearch.index", String.class);
    private String doc_type = ConfigProvider.getConfig().getValue("elasticsearch.doc_type", String.class);

   // code

}

This is one way of configuring the data. There are other ways to do so that you can find here.

Service Invocation

Now that we've defined the RESTful APIs to access the Catalog microservice and done the necessary configurations, let us now look at the code to invoke the Inventory microservice to retrieve the latest list of inventory items.

First, create an interface that represents the remote service using JAX-RS annotations. Declare the APIs as part of this interface and annotate them with the org.eclipse.microprofile.rest.client.inject.RegisterRestClient annotation.

import javax.ws.rs.GET;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

@RegisterRestClient
@RegisterProvider(InventoryResponseExceptionMapper.class)
public interface InventoryServiceClient {
        
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    List<Item> getAllItems() throws UnknownUrlException, ServiceNotReadyException;

}

To handle exceptions, define a ResponseExceptionMapper. To register the provider, use the @RegisterProvider annotation on the interface. 

So, we saw the Catalog microservice API definition, and our next step is to examine how we use the REST client:

java
import javax.inject.Inject;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import ibm.cn.application.client.InventoryServiceClient;

public class InventoryRefreshTask extends Thread {
        
        @RestClient
        @Inject
        private InventoryServiceClient invClient;
       
        // code
}

In order to use the Rest Client, we should use the javax.inject.Inject and org.eclipse.microprofile.rest.client.inject.RestClient annotations. 

And finally, we should then configure the Rest Client by adding the name of the property in the application.properties file. For this, to name the property, we should prepend the fully qualified name of the interface to the /mp-rest/url keyword:

ibm.cn.application.client.InventoryServiceClient/mp-rest/url = http://${INVENTORY_HOST_NAME:localhost}:${INVENTORY_PORT:8082}/micro/inventory

Resilience

Quarkus implements all its fault tolerance policies using SmallRye Fault Tolerance, which is an implementation of the MicroProfile Fault Tolerance specification. The fault tolerance specification defines the following recovery procedures:

  • Circuit breaker: Provides a fail-fast approach in case of overload or non-availability.
  • Bulkhead: The workload is limited to a microservice to prevent failures caused by concurrency or service overload.
  • Fallback: If the annotated method cannot be executed, it executes an alternative method.
  • Retry policy: Specifies the conditions for retrying an unsuccessful execution.
  • Timeout: Specifies the maximum execution time before interrupting a request.

Let's look into the Catalog microservice and see how we added resilience to the service:

So in the code above, the getInventory method retries up to two times for, at most, 2,000 seconds. Even with this configuration, the request may still fail sometimes. And when the request fails, it is important to respond with something meaningful. In order to do that, an alternative method — fallbackInventory — is defined and this menthod will be executed whenever an exception raises.

Along with the fault tolerance, making sure if an instance of service is still functioning properly is equally critical. For this, Quarkus uses the SmallRye Health specification. Service mechanisms communicate their current status to the orchestration mechanism, which then allows the system to act accordingly. For this purpose, two dedicated endpoints — /health/ready and /health/live — are used.

For the Catalog microservice, Liveness health check is defined as follows:

import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Liveness;

@Liveness
public class LivenessCheck implements HealthCheck {

        @Override
        public HealthCheckResponse call() {
                // TODO Auto-generated method stub
                return HealthCheckResponse.named("Liveness Check for Catalog Service")
                .withData("status", "live")
                .up()
                .build();
        }
}

And the definition for the Readiness health check is as follows:

import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Readiness;

@Readiness
public class ReadinessCheck implements HealthCheck {

        @Override
        public HealthCheckResponse call() {
                // TODO Auto-generated method stub
                return HealthCheckResponse.named("Readiness Check for Catalog Service")
                .withData("status", "ready")
                .up()
                .build();
        }
}

Let us validate the Liveness and Readiness health checks as follows:

$ curl localhost:8080/q/health/live
{
    "status": "UP",
    "checks": [
        {
            "name": "Liveness Check for Catalog Service",
            "status": "UP",
            "data": {
                "status": "live"
            }
        }
    ]

$ curl localhost:8080/q/health/ready
{
    "status": "UP",
    "checks": [
        {
            "name": "Readiness Check for Catalog Service",
            "status": "UP",
            "data": {
                "status": "ready"
            }
        }
    ]
}

Monitoring

Quarkus allows us to monitor the operation of our microservices by using metrics and distributed tracing. 

First, let us look into the metrics. To generate metrics, we use the quarkus-smallrye-metrics extension and this, in turn, implements the MicroProfile Metrics specification. These metrics are exposed at the /metrics base path and are available in two different formats — one is the JSON format and the other is the Prometheus text format.

Below are the custom metrics defined in the Catalog service:

import java.io.IOException;
import java.util.List;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.eclipse.microprofile.metrics.MetricUnits;
import org.eclipse.microprofile.metrics.annotation.Counted;
import org.eclipse.microprofile.metrics.annotation.Timed;

@Path("/micro/items")
public class CatalogResource {
       
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Timed(name = "callstoInventory", description= "How long it takes to list inventory items", unit = MetricUnits.MILLISECONDS)
    @Counted(name = "InventoryListTimer", description = "How many requests happened to list inventory items")
    public List<Item> getInventory() throws IOException {
            //code
    }
}

We can access the overall metrics on the /metrics endpoint. These are basically categorized into three different scopes. If you want to access them individually, below are the endpoint details:

  • Base metrics can be accessed at /metrics/base, and these metrics include JVM statistics like the current heap sizes, garbage collection times, thread counts and other OS and host system information.
  • Vendor metrics can be accessed at /metrics/vendor and these metrics include vendor-specific data like OSGi statistics.
  • Application metrics can be accessed at /metrics/application and these include custom metrics defined by the application developer.

Below is a list of custom metrics on the /metrics/application endpoint for the Catalog microservice:

$ curl localhost:8080/q/metrics/application
# TYPE application_ft_ibm_cn_application_CatalogResource_getInventory_invocations_total counter
application_ft_ibm_cn_application_CatalogResource_getInventory_invocations_total 1.0
# TYPE application_ft_ibm_cn_application_CatalogResource_getInventory_retry_callsSucceededNotRetried_total counter
application_ft_ibm_cn_application_CatalogResource_getInventory_retry_callsSucceededNotRetried_total 1.0
# HELP application_ibm_cn_application_CatalogResource_InventoryListTimer_total How many requests happened to list inventory items
# TYPE application_ibm_cn_application_CatalogResource_InventoryListTimer_total counter
application_ibm_cn_application_CatalogResource_InventoryListTimer_total 1.0
# TYPE application_ibm_cn_application_CatalogResource_callstoInventory_rate_per_second gauge
application_ibm_cn_application_CatalogResource_callstoInventory_rate_per_second 2.6552945299247765E-4
# TYPE application_ibm_cn_application_CatalogResource_callstoInventory_one_min_rate_per_second gauge
application_ibm_cn_application_CatalogResource_callstoInventory_one_min_rate_per_second 1.8950510984410557E-29
# TYPE application_ibm_cn_application_CatalogResource_callstoInventory_five_min_rate_per_second gauge
application_ibm_cn_application_CatalogResource_callstoInventory_five_min_rate_per_second 1.3614856728453771E-8
# TYPE application_ibm_cn_application_CatalogResource_callstoInventory_fifteen_min_rate_per_second gauge
application_ibm_cn_application_CatalogResource_callstoInventory_fifteen_min_rate_per_second 1.7761016468922385E-5
# TYPE application_ibm_cn_application_CatalogResource_callstoInventory_min_seconds gauge
application_ibm_cn_application_CatalogResource_callstoInventory_min_seconds 0.041877072
# TYPE application_ibm_cn_application_CatalogResource_callstoInventory_max_seconds gauge
application_ibm_cn_application_CatalogResource_callstoInventory_max_seconds 0.041877072
# TYPE application_ibm_cn_application_CatalogResource_callstoInventory_mean_seconds gauge
application_ibm_cn_application_CatalogResource_callstoInventory_mean_seconds 0.041877072
# TYPE application_ibm_cn_application_CatalogResource_callstoInventory_stddev_seconds gauge
application_ibm_cn_application_CatalogResource_callstoInventory_stddev_seconds 0.0
# HELP application_ibm_cn_application_CatalogResource_callstoInventory_seconds How long it takes to list inventory items
# TYPE application_ibm_cn_application_CatalogResource_callstoInventory_seconds summary
application_ibm_cn_application_CatalogResource_callstoInventory_seconds_count 1.0
application_ibm_cn_application_CatalogResource_callstoInventory_seconds{quantile="0.5"} 0.041877072
application_ibm_cn_application_CatalogResource_callstoInventory_seconds{quantile="0.75"} 0.041877072
application_ibm_cn_application_CatalogResource_callstoInventory_seconds{quantile="0.95"} 0.041877072
application_ibm_cn_application_CatalogResource_callstoInventory_seconds{quantile="0.98"} 0.041877072
application_ibm_cn_application_CatalogResource_callstoInventory_seconds{quantile="0.99"} 0.041877072
application_ibm_cn_application_CatalogResource_callstoInventory_seconds{quantile="0.999"} 0.041877072

For distributed tracing, Quarkus uses the quarkus-smallrye-opentracing specification. This specification implements the OpenTracing API which, in turn, is based on Jaeger for distributed tracing.

To view the call traces, access the Jaeger UI. One of the traces for the Catalog microservice is as follows:

To view the call traces, access the Jaeger UI. One of the traces for the Catalog microservice is as follows:

For more details on how the Catalog service is built, have a look here and for more details on how the Inventory service is built, have a look here

Next steps

Quarkus allows us to quickly and easily create solutions. Quarkus is way ahead of the game, offering a huge list of extensions for database access, messaging, REST APIs and so on. We can, therefore, be assured that cloud-native Java is a viable choice. Also, for those who are developing microservices and want to deploy them on Kubernetes, Quarkus is a good choice because it seamlessly integrates with Kubernetes. 

The exemplar microservices are simple and can be quickly implemented with just few lines of code. The source code is available on GitHub. For instructions on how to run it, please refer to the Readme.md file.

The Quarkus guides were a breeze; they are easy to understand and making use of them eased our path.

Throughout this blog, we demonstrated how we used Quarkus to implement RESTful APIs, Configurations, Invocations, Resilience and Monitoring for our StoreFront application. But this is just the beginning — Quarkus can do much more. 

In the next blog post, we will cover authentication and application security using Keycloak. Meanwhile, stay tuned and take a look at our cloud native reference implementation available here.

Be the first to hear about news, product updates, and innovation from IBM Cloud