Our journey with Quarkus

18 January 2022

8 min read

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

The Java (link resides outside ibm.com) 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 (link resides outside ibm.com).

Quarkus is a full-stack, cloud-native Java framework developed by Red Hat® (link resides outside ibm.com). It supports Java Virtual Machine (JVM) as well as native compilation (link resides outside ibm.com). Quarkus is based on MicroProfile (link resides outside ibm.com) standard and some Jakarta EE (link resides outside ibm.com) 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 (link resides outside ibm.com) cloud-native journey to Red Hat® OpenShift® (link resides outside ibm.com) .

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:

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 (link resides outside ibm.com) 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 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 (link resides outside ibm.com) returns a list of all the antique computing devices that are available. This microservice uses a MySQL (link resides outside ibm.com) database as its datasource. The Catalog microservice (link resides outside ibm.com) uses Elasticsearch and serves as cache to the Inventory microservice:

RESTful APIs

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 (link resides outside ibm.com) 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 toapplication/json.

Configuration

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

Theapplication.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 (link resides outside ibm.com).

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 (link resides outside ibm.com), 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, thegetInventory 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 (link resides outside ibm.com) 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 (link resides outside ibm.com) 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 (link resides outside ibm.com) format and the other is the Prometheus text format (link resides outside ibm.com).

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 (link resides outside ibm.com) specification. This specification implements the OpenTracing API which, in turn, is based on Jaeger (link resides outside ibm.com) for distributed tracing.

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 (link resides outside ibm.com) and for more details on how the Inventory service is built, have a look here (link resides outside ibm.com). 

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 (link resides outside ibm.com) file.

The Quarkus guides (link resides outside ibm.com) 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 (link resides outside ibm.com). Meanwhile, stay tuned and take a look at our cloud native reference implementation available here (link resides outside ibm.com).

Author

Hemankita Perabathini

Cloud Solution Architect