Lightweight Micronaut for Java-based microservices

Posted By
Nikhil Wadkhelkar

For many years, JAVA has been a compelling and enterprise-level programming language. Its long-used and traditional framework, like Spring framework, was introduced in the era when applications were designed to be monoliths and were mostly deployed on-prem servers. But today, this scenario has drastically changed with the introduction of Micronaut.
The modern development pattern has shifted from monolithic architecture to microservice architecture. Containers are replacing large servers with high memory limits. On-prem servers are being shifted to cloud-native. Some applications prefer serverless functions that use frequent cold starts instead of one-time startups. Java frameworks also need to evolve themselves to match up with these evolving changes.
What is the Micronaut framework?
Micronaut is an open-source, modern, and JVM-based framework. It is designed to build serverless applications and microservice applications, which are lightweight, fast, and scalable. This framework was created to make the microservice creation process quick and easy, with low memory consumption and built-in cloud-native development functionality.
Challenges of traditional Java frameworks like Spring
Let’s go through some of the major cons of using a traditional Java framework like Spring or Spring Boot:
- Slow startup: Spring's heavy reliance on reflection and runtime dependency injection contributes to increased startup time. This longer startup time significantly impacts applications that are serverless and/or based on microservice architecture, which may necessitate frequent redeployment depending on the scenario.
- High memory consumption: In Spring, during runtime compilation, unused beans and classes are also loaded into memory, leading to increased memory consumption and, consequently, higher cloud costs.
- Native image integration: Integrating with native images is challenging because the Spring framework, which relies heavily on reflection, doesn't function properly with native images.
For those looking to enhance Spring Boot’s capabilities, explore how event-driven architecture with Spring Boot can improve scalability and responsiveness.
Spring is continuously evolving its ecosystem to meet modern application development needs, such as serverless, cloud-native, and microservices, through solutions like Spring Native and Spring Boot 3 with Ahead-of-Time (AOT) compilation. However, these solutions still have some limitations compared to other new alternative frameworks available in the current techno-space. Some of these alternatives are:
- Micronaut
- Quarkus
- Helidon
We'll focus on the Micronaut framework in this blog.
Key features of Micronaut for microservices and serverless
- Compile-time Dependency Injection (DI) mechanism / AOT Compilation: Unlike the Spring framework, which uses reflection for dependency injection at runtime, this framework performs DI, AOP, and configuration parsing at compile-time itself. This improves the startup time of the application to almost become an instant startup with minimized runtime overhead. Since Micronaut provides compile-time DI, it can be easily coupled with GraalVM.
- Minimal or no use of reflection or proxies: Micronaut does not use Java reflection anywhere in its framework and thus provides improved performance and memory usage. This benefit of Micronaut makes it more suitable and demanding for serverless and container-based environments.
- Cloud-native: Since day one of its inception, Micronaut has been designed and built to run in cloud environments. It provides built-in support for various cloud providers like AWS, Azure, and GCP. Along with this, it also supports tools like Eureka, reactive HTTP clients, and servers. It has
- Polyglot framework: Micronaut's compatibility with the Java language, as well as other languages like Groovy and Kotlin, makes it a Polyglot framework.
- Ecosystem-friendly: It can be smoothly and efficiently integrated with the application’s ecosystem parts, such as the data-access layer and various messaging queue systems, including Kafka and RabbitMQ. It also provides the ability to integrate with OpenAPI and security protocols like JWT and OAuth2.
To dive deeper into securing Java applications, learn about custom authentication with Spring Security for robust protection.
Steps to setting up a currency converter application with Micronaut
Let’s have a quick walk-through on how we can use the Micronaut framework to set up a basic "Currency Converter" application.
Installing Micronaut on your system
Based on the operating system you are using, the installation process varies slightly.
- Installing Micronaut on Unix-based platforms
The best, quickest, and easiest way to install Micronaut on a Unix-based platform is by using SDKMAN! (Software Development Kit Manager). So, make sure your system has SDKMAN installed on it. You can check whether SDKMAN is present on your system or not by running the command below
$ sdk version
If SDKMAN is installed on your system, you will see the version of SDKMAN in the output; else, you will see something like
sdk: command not found
If SDKMAN is not installed on your system, then run the following command:
$ curl –s https://get.sdkman.io | bash
If SDKMAN is installed, you can proceed to install Micronaut. For that, you can run the following command:
$ sdk install micronaut {version}
In the above command, you can specify any specific version you want to install. After running the above command, Micronaut will be installed on your system, and you will now be able to run the Micronaut CLI on your system.
- Installing Micronaut on Windows-based systems
The simplest and easiest way to install Micronaut on Windows-based systems is by downloading binary from Micronaut’s official website (https://micronaut.io/download/).
- Once this binary is downloaded, extract it to the appropriate location.
- Create an environment variable as MICRONAUT_HOME pointing to your installation directory of the binary and update the PATH environment variable by appending %MICRONAUT_HOME%\bin.
And that’s it. Now you will be able to run the Micronaut CLI on your computer.
Micronaut can also be installed on Windows using Chocolatey package manager.
To verify the successful installation of Micronaut on your system, you can run the following command:
$ mn --version
If Micronaut CLI is installed successfully on your system, then you will see the version of Micronaut in the output.
Steps to create your first Micronaut application
Step-1: Generating/creating a new Micronaut project
There are mainly two ways to create a new Micronaut project. One is using the Micronaut CLI, and the other is using the Micronaut Launch website. To create a project using CLI, run the command below:
mn create-app com.blog.currencyconverter --build=gradle --lang=java
Here, mn is used to invoke Micronaut CLI, followed by create-app, which tells CLI to create a new application as specified ahead.
com.blog.currencyconverter is used to define the package (com.blog) + project name (currencyconverter).
--build is used to specify the type of build tool you want to use for your application. As of now, Micronaut supports Gradle and Maven as building tools.
--lang is used to specify the programming language that you want to use for your application. Currently, it supports Java, Kotlin, and Groovy.
After running the above command successfully, it will create a new folder named currencyconverter.
To create a project using the Micronaut Launch website: Visit https://micronaut.io/launch, where you will be able to configure and generate a new Micronaut project quickly. This is similar to Spring Initializr. Once the project is generated, a .zip file will be generated. Extract that file to an appropriate location.
Now, once the project folder is generated, you can go into the folder and then run the project with the following command:
For running on Unix/Linux/macOs/Windows(Git bash):
- For Gradle: ./gradlew run
- For Maven: ./mvnw mn:run
For running on Windows cmd:
- For Gradle: gradlew run
- For Maven: mvnw mn:run
Step-2: Implementing the layered architecture for the currency converter
As per the traditional layered architecture flow, create a REST controller, service class, and supporting model classes for our current application.
(i) Controller layer (CurrencyConverterController.java)
package com.blog.controller; import com.blog.model.CurrencyConverterDto; import com.blog.model.Response; import com.blog.service.ConverterService; import io.micronaut.http.HttpResponse; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.QueryValue; import io.micronaut.scheduling.TaskExecutors; import io.micronaut.scheduling.annotation.ExecuteOn; @Controller("/convert") public class CurrencyConverterController { private final ConverterService converterService; public CurrencyConverterController(ConverterService converterService) { this.converterService = converterService; } @ExecuteOn(TaskExecutors.BLOCKING) @Get public HttpResponse<CurrencyConverterDto> convertCurrency(@QueryValue String from, @QueryValue String to, @QueryValue double amount, @QueryValue String accessKey) { long startTime = System.currentTimeMillis(); Response response = converterService.convertCurrency(from, to, amount, accessKey); long endTime = System.currentTimeMillis(); System.out.println("Total client API time: "+ (endTime-startTime)); CurrencyConverterDto ccDto = new CurrencyConverterDto(); ccDto.setFrom(from); ccDto.setTo(to); ccDto.setAmount(amount); ccDto.setConvertedAmount(response.getResult()); System.out.println("Total micronaut API time: "+ (System.currentTimeMillis() - startTime)); Runtime runtime = Runtime.getRuntime(); runtime.gc(); long usedMemory = (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024); System.out.println("**Used memory: " + usedMemory + " MB"); return HttpResponse.ok(ccDto); } }
Here, @Controller is used to define the route for the API. It is similar to Spring’s @Controller or @RestController in Spring Boot. @Get is used for GET HTTP method. @ExecuteOn(TaskExecutors.BLOCKING) is used to specify that the annotated class or method should be executed as separate blocking I/O thread.
Why is this annotation needed?
By default, Micronaut is a non-blocking and reactive framework. It uses something like Netty, which is designed around a non-blocking I/O and Event loop model. This Event loop model makes Micronaut execute the code in the Controller layer (defined in the @Controller bean) in the same thread as the request thread. This way, it handles requests very fast and in an asynchronous way.
However, if the application flow contains any blocking I/O operations, such as HTTP client calls or JDBC calls, it will throw a Runtime Exception. So, to avoid this and to make our API request successful, @ExecuteOn(TaskExecutors.BLOCKING) can be used.
(ii) Service layer (ConverterService.java)
package com.blog.service; import com.blog.model.Response; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.QueryValue; import io.micronaut.http.client.annotation.Client; @Client("https://api.exchangerate.host") public interface ConverterService { @Get("/convert") Response convertCurrency(@QueryValue String from, @QueryValue String to, @QueryValue double amount, @QueryValue("access_key") String accessKey); }
Here, @Client is the annotation provided by Micronaut, which acts as a declarative HTTP client to communicate with other APIs. By default, @Client uses a blocking HTTP client. Therefore, if it is used inside a controller (i.e., Event loop thread), it needs to be handled with @ExecuteOn(TaskExecutors.BLOCKING) for uninterrupted request flow.
In the above example, the cloud-based public API of one of the Online Currency Exchange Services has been used for converting the currency. Since Micronaut performs annotation processing at compile time and not runtime, it processes @Client annotation and generates HTTP client code ahead of time. This symbolizes one of Micronaut's key features: its cloud-native design.
(iii) Supporting model package (CurrencyConverterDto.java, Response.java)
package com.blog.model; import io.micronaut.serde.annotation.Serdeable; @Serdeable public class CurrencyConverterDto { private String from; private String to; private double amount; private double convertedAmount; private boolean status; //Getters & Setters for the fields public String getFrom() { return from; } public void setFrom(String from) { this.from = from; } public String getTo() { return to; } public void setTo(String to) { this.to = to; } public double getAmount() { return amount; } public void setAmount(double amount) { this.amount = amount; } public double getConvertedAmount() { return convertedAmount; } public void setConvertedAmount(double convertedAmount) { this.convertedAmount = convertedAmount; } public boolean isStatus() { return status; } public void setStatus(boolean status) { this.status = status; } }
package com.blog.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @JsonIgnoreProperties(ignoreUnknown = true) public class Response { private boolean success; private double result; public boolean isSuccess() { return success; } public void setSuccess(boolean success) { success = success; } public double getResult() { return result; } public void setResult(double result) { this.result = result; } }
Here, the use of @Serdeable annotation tells the Micronaut framework that this class can be serialized and deserialized. This is a replacement for Jackson’s @JsonSerialize/@JsonDeserialize by Micronaut, which works at compile time, making it more efficient.
Step-3: Running the application
Once all coding parts are completed, run the application using the earlier-mentioned command. After successfully running the application, you will see logs like:
Step-4: Testing the currency conversion API
After a successful start of the application, you can test your API by hitting it via any API testing tool like Postman. It should return an expected response based on your implementation.
Micronaut Vs. Spring Boot framework
To compare the performance of Micronaut with Spring Boot, I created an exact copy of the project using the Spring Boot framework, incorporating the required changes as per the framework, and utilized WebClient for communication with the external API.
And here are the key performance differences I found between these two frameworks:
From the above comparison, it is clear how different parameters give Micronaut an edge over Spring Boot. Although the above parameters highlight the strength of using the Micronaut framework, this does not mean that all applications should switch to the Micronaut framework over other frameworks. So, this leads to our last question of this blog:
When to choose Micronaut over Spring or Spring Boot?
Here’s how to decide if Micronaut can be a better fit based on the real-world requirements:
- Faster or quicker startup time: Applications built with Micronaut start in tens of milliseconds, thanks to compile-time dependency injection and the absence of reflection. Whenever it is required to build serverless applications or functions, Micronaut will be a better fit as it is designed for efficiency and performance. Also, this feature makes it a suitable candidate for applications which require rapid cold starts in Kubernetes.
- Low memory consumption: Since Micronaut is designed for Ahead-of-Time compilation, it initializes everything at compile-time, eliminating the need for runtime reflection data in memory, which results in a low memory footprint for the Micronaut framework. This feature of Micronaut makes it ideal for building lightweight, decoupled systems, which will give better performance, reduced cost for running apps on cloud VMs, containers, etc.
- To build microservices quickly: When it comes to building microservices, efficiency and performance are the major priorities, and Micronaut is the perfect candidate for the same. Micronaut is designed to be microservice-first. It provides built-in support for service discovery, such as Eureka and Consul, along with load balancing and fault tolerance capabilities.
- Cloud-native and serverless: Micronaut's in-built support for GraalVM enables the creation of native images that are quicker and significantly smaller than traditional JVM-based applications. This provides the key advantage to Micronaut as a primary choice for serverless platforms like AWS Lambda.
Why choose Micronaut for modern applications?
Choosing the proper framework for your application can make or break your application’s performance, scalability, and cost-efficiency. Suppose you are aiming to build an application that demands a quick startup time, a low memory footprint, and seamless native image support. In that case, Micronaut can be a great choice as it provides compile-time dependency injection, no reflection, and built-in support for reactive programming. This makes Micronaut the ideal candidate for microservices and serverless architecture.
Since Micronaut provides built-in support for reactive programming approaches, this helps in enhancing responsiveness and resource efficiency, especially in applications where scalability matters, like I/O bound systems. However, the decision to use this depends on various factors like project goals, the team’s expertise, and the need for the ecosystem. Looking to build cutting-edge microservices? Leverage Opcito’s software product engineering services to create scalable, high-quality applications.
Ultimately, frameworks like Micronaut empower developers to design and build lightweight, faster, and more efficient applications. Suppose you are planning a new project that requires a quick startup time. If you're looking for improved performance with scalability and cloud-native flexibility, Micronaut is worth considering. Contact a Micronaut specialist at contact@opcito.com for more information.