How to Efficiently Create Microservices: An Overview of Jakarta EE and MicroProfile

If you want to develop microservices or backend applications in Java and make use of innovative and new technologies while also reducing coding effort, speeding development time, and producing a more resilient application, you should consider using Jakarta EE and MicroProfile. In this article, we’ll look at Jakarta EE and MicroProfiles and how they work together to make a powerful tool for building microservices. 

What is Jakarta EE? 

Since 1999 when it was first introduced, Java EE has become the foundation of a great deal of enterprise technology. In 2017, Oracle transferred development of Java Enterprise Edition (Java EE) to the open source Eclipse Foundation. Renaming the product Jakarta EE (Oracle retained the Java trademark), the Eclipse Foundation has been hard at work ever since completing the transfer in a phased approach.

Jakarta EE 8.0 was released in 2017, and was the first release from the Eclipse Foundation. Jakarta EE 9.0 will be released in 2020 and will mark the full transfer of Java EE 8.0 to Eclipse. After this point, it’s good-bye Java EE and hello, Jakarta EE!

What is a MicroProfile? 

MicroProfile is an open-source initiative, also managed by the Eclipse Foundation. MicroProfile optimizes Enterprise Java for microservices. 

The latest release at the time of this writing is MP 3.3, and it provides a lot of nice features out of the box, without complex configurations, including: OpenAPI, Open Tracing, Health Check, Metrics, Config API, Fault Tolerance, JWT Authentication, Rest Client, and even a new GraphQL module to provide and consume GraphQL APIs. You can see the code here

The Best of Both Worlds: Standardization and Innovation

Looking at both projects, you might think they overlap. I find the easiest way to differentiate them is this: Jakarta EE will focus on standardization through the specifications, and MicroProfile will focus on innovation. When a specification of MicroProfile is mature enough, it will be converted to a standard and become part of Jakarta EE. As technologies change in the future, MicroProfile will have room to work on more innovative features, providing the best tools to the Jakarta EE developers.

Let’s Get Started!

The best way to understand how these two technologies interact with each other and how we can take advantage of them is to look at some code! We’ll use the projects in this repository, and we’ll see how, with minimum effort, we can use them to get several functionalities.

Our example application consists of two web applications: the items-service and the stock-service. The items-service returns data about different types of items (books, groceries). The stock-service returns stock information about the items (availability, price, status). The items-service consumes the stock-service to get the stock information of an item.

Our Example App From the Jakarta EE Side

From the Jakarta EE side, we’ll take advantage of JAX-RS to create Restful services easily, and CDI to implement our service classes with business logic. In a real application, we can also use the JPA and JTA specifications to get access to a database. For this blog, though, we won’t dig into JPA and JTA.

1. pom.xml

   <dependency>
    <groupId>jakarta.platform</groupId>
    <artifactId>jakarta.jakartaee-api</artifactId>
    <version>8.0.0</version>
    <scope>provided</scope>
</dependency>

2. items-service/BookResource.java

@ApplicationScoped
@Path("/books")
public class BookResource{

   @Inject
   private BookService service;
    
   @GET
   @Produces(MediaType.APPLICATION_JSON)
   public List getAll() {
      return service.getAll();
   }
   @GET
   @Produces(MediaType.APPLICATION_JSON)
   @Path("/{id}") 
   public Book findById(@PathParam("id") Long id) {
      return service.findById(id)
            	.orElseThrow(NotFoundException::new);
   }
Our App From the Microprofile Side

Now it’s time to MicroProfile! We enable the basic MicroProfile features by just adding the following dependency to the project.

1. Pom.xml

   <dependency>
    <groupId>org.eclipse.microprofile</groupId>
    <artifactId>microprofile</artifactId>
    <version>3.3</version>
    <type>pom</type>
    <scope>provided</scope>
</dependency>
MicroProfile: Fault Tolerance

This is my favorite MicroProfile module, because it reduces the amount of code and provides really nice features to make our application more resilient, including these fault tolerance policies: Timeout, Retry, Fallback, Bulkhead, and CircuitBreaker.

In the following example, we can see how to use some of them. 

1. items-service/StockService.java

@Timeout(value = 5000)
@Retry(maxRetries = 3, delay=500)
@Fallback(applyOn = CircuitBreakerOpenException.class, fallbackMethod = "getStockFallBack")
@CircuitBreaker(successThreshold = 10, requestVolumeThreshold = 4, failureRatio=0.75,delay = 1000)
public Stock getStock(String id){
    	URI apiUri = getURI();
    	StockClient stockClient = RestClientBuilder.newBuilder()
        	.baseUri(apiUri)
        	.build(StockClient.class);
    	return stockClient.findById(id);
}

• The @Timeout annotation prevents waiting forever during the execution of the method. 

• The @Retry annotation will invoke the method up to three times, in case an error occurs, with each invocation having 300ms of delay. This is very useful to recover from a brief network glitch.

• The @CircuitBreaker annotation prevents repeating invocations of a logic that is failing constantly. For example, the other service might be down. If out of four requests, 75% fail (or three requests fail), then the circuit is open and it will wait for 1000ms to open; then it will be half-open until 10 successful invocations or the circuit is open again. When the circuit is opened, a CircuitBreakerOpenException is thrown.

• The @Fallback annotation helps us to recover from an exception (unexcepted, circuitbreaker, timeout, and more). In the example, we are returning null when the application gets a CircuitBreakerOpenException.

2. Now let’s run a failure scenario. If the stock-service is down because we are using the fault tolerance annotations, our application will retry three times. That call won’t exceed five seconds, and after three fails, the circuit breaker is opened, the fallback action is triggered, and we will have a null value as a result.  

Our console/logs will look like:

Getting stock for stockId 000001 has failed. The Circuit breaker is open|#]
Getting stock for stockId 000002 has failed. The Circuit breaker is open|#]
Getting stock for stockId 000003 has failed. The Circuit breaker is open|#]

And the JSON result if we call the list books resource (http://localhost:8080items-service/api/books/) looks like: 

So, our application is resilient enough to handle network issues or even when other required services are down, with a minimum effort and just adding a few annotations.  

MicroProfile Rest Client

This module lets us implement a simple client for a Restful service that we need to consume. We just need to create an interface with JAX-RS annotations describing the behavior of the Restful service.

1. items-service/StockClient.java

@Path("/stock")
public interface StockClient {
   @GET
   @Produces(MediaType.APPLICATION_JSON)   	 
   List getAll();
    
   @GET
   @Produces(MediaType.APPLICATION_JSON)
   @Path("/status/{status}")   	 
   List getByStatus(@PathParam("status")String status);
    
   @GET
   @Path("/{id}")
   @Produces(MediaType.APPLICATION_JSON)
   Stock findById(@PathParam("id") String id);
}

And then, we can use the RestClientBuilder and our client interface to generate a proxy instance to call the methods in a nicer way. 

2. items-service/StockService.java

public Stock getStock(String id){
   URI apiUri = getURI();
   StockClient stockClient = RestClientBuilder.newBuilder()
       .baseUri(apiUri)
       .build(StockClient.class);
   return stockClient.findById(id);
}
MicroProfile Config 

This module is simple yet powerful at the same time. It allows us to separate the configuration values from our code, so we won’t need to repackage the application each time the underlying runtime environment changes.

1. items-service/microprofile-config.properties

message.hello = Hello to MP-Config
stockservice.api.url = http://localhost:8080/stock-service/api

2. items-service/HelloResource.java

@Path("/hello")
public class HelloResource {
    
   @Inject @ConfigProperty(name = "message.hello")
   private String message;
    
   @GET
   public String message(){
      return message;
   }
}

Accessing the /api/hello endpoint, the message will be seen.

Another use of the MicroProfile Config module is to inject the url of other services that our application needs to consume.

1. items-service/StockService.java

@Inject @ConfigProperty(name = "stockservice.api.url")
private String apiUrl;

MicroProfile Config is going to search the System Properties, Environment Variables, and microprofile-config.properties file for the value to inject. In a containerized environment, we might want to use environment variables to populate the configuration values of our application.

MicroProfile Open API 

Right out of the box, MicroProfile provides support to Open API and generates a YAML file with the details of our Restful services; and we can see it at /openapi endpoint.

Open API allows us to add more details to our Restful resources like descriptions, status codes, return examples, details of parameters, and more by adding some annotations in our code. Let’s see an example using the findById method into the BookResource class in the items-service application.

1. items-service/BookResource.java

@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
@APIResponses(
   value = {
   @APIResponse(
      responseCode = "404",
      description = "Book not found"),
   @APIResponse(
      responseCode = "200",
      description = "Book is found and returned",
      content = @Content(schema = @Schema(implementation = Book.class)))
})
@Operation(
   summary = "Finds a book by its Id",
   description = "Finds a book by its Id … more details…")
public Book findById(
 @Parameter(description = "Book’s Id", required = true) @PathParam("id") Long id) {
   return service.findById(id).orElseThrow(NotFoundException::new);
}

2. Then I recommend adding the following dependency to provide a Swagger-UI page displaying the details into our openapi.yaml file. 

<dependency>
    <groupId>org.microprofile-ext.openapi-ext</groupId>
    <artifactId>swagger-ui</artifactId>
    <version>1.0.3</version>
    <scope>runtime</scope>
</dependency>

3. It’s time to see the documentation of our Resources, so we just need to access the endpoint /<appcontext>/api/openapi-ui. In our case, http://localhost:8080/items-service/api/openapi-ui

MicroProfile Health Check

When the amount of services increases, it gets more complex to detect and handle whether other services instances are running correctly or not. One solution is that every service should provide a health check with its status and the status of its components. 

To create health checks with MicroProfile, we just need to create a class that implements from HealthCheck and overrides the HealthCheckResponse call() method. Also, the class requires at least one of the annotations: @Liveness or @Readiness. We’ll code the logic that checks that the component of our service is working correctly into the call method. 

1. stock-service/DatabaseHealthCheck.java

import javax.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.HealthCheckResponseBuilder;
import org.eclipse.microprofile.health.Liveness;
import org.eclipse.microprofile.health.Readiness;

@ApplicationScoped
@Liveness
@Readiness
public class DatabaseHealthCheck implements HealthCheck {

   @Override
   public HealthCheckResponse call() {
    	HealthCheckResponseBuilder responseBuilder = HealthCheckResponse.named("database");
    	try {
        	return responseBuilder.up()
                	.withData("name", "my_dummy_db")
                	.build();
    	}catch(Exception ex){//SqlException
        	return responseBuilder.down().build();
    	}
  }
}

All our health checks are available at the URL: http://localhost:8080/health

{  
 "status":"UP",
  "checks":[            
      {         
         "name":"database",
         "status":"UP",
         "data":{
            "Name":"my_dummy_db"
      }]   
}


2. I recommend adding the following dependencies that provide some generic health checks so you can know the memory and thread usage of our application, and to publish a simple dashboard UI with a summary of our health checks. That UI is available at: http://localhost:8080/<app-context>/health-ui/:

<dependency>
    <groupId>org.microprofile-ext.health-ext</groupId>
    <artifactId>healthprobe-jvm</artifactId>
    <version>1.0.5</version>
    <scope>runtime</scope>
</dependency>
     
<dependency>
    <groupId>org.microprofile-ext.health-ext</groupId>
    <artifactId>health-ui</artifactId>
    <version>1.0.5</version>
    <scope>runtime</scope>
 </dependency>
 

http://localhost:8080/stock-service/health-ui/

MicroProfile: Metrics

How would you like to know things like how many times a service has been called, or how many concurrent executions a service has at this moment, or how long it takes a command to be executed? 

The MicroProfile Metrics module provides a simple and unified way to publish those and other metrics that you can use to monitor the performance of the application. They are exported in Prometheus format, which means you can use tools like Prometheus or Grafana dashboards to display the data and alerts. 

We can get some metrics in our code using the annotations: @Counted, @Timed, @Metered, and @Gauge. We can also create custom metrics by using the annotation @Metric and creating custom histograms.

1. stock-service/StockService.java

@Counted(name="StockService.getAll_counter")
@Timed(name="StockService.getAll_timer")
public List getAll(){
   //some code here
}
    
@Timed
public List getByStatus(String status){
   //some code here
}

@Metered
public Optional findById(String id){
   //some code here
}

2. Finally, we can access the endpoint /metrics/application to get all the metrics exposed by our application. http://localhost:8080/metrics/application

Create Microservices With Less Effort

Jakarta EE and MicroProfile combined give us a powerful way to create microservices while reducing boilerplate code and still providing top-notch features with minimum effort. And you get a resilient application that can easily scale to the needs of your business. You should definitely consider using what we’ve learned here in your future applications, and I encourage you to continue exploring what’s possible using the resources I’ve included below.

References 

https://github.com/eclipse/microprofile

https://www.baeldung.com/java-enterprise-evolution

https://www.tomitribe.com/blog/overview-of-microprofile-rest-client/

https://www.eclipse.org/community/eclipse_newsletter/2017/september/article4.php

https://github.com/AdamGamboa/JakartaAndMicroProfile

https://blog.adamgamboa.dev/2020/05/12/lets-meet-jakarta-ee/

 

Adam Gamboa

Adam is a Java developer with 8 years of experience oriented towards Java EE, back-end and Rest API development.

Related Articles

Ready to be Unstoppable?

Partner with Gorilla Logic, and you can be.