Building Microservices with Micronaut

A lightweight framework designed from the ground up for microservices and serverless computing

March 15, 2019

Download a PDF of this article

Micronaut is a microservices framework that is especially designed for the development of modular, easy-to-test applications that embrace the 12-factor design orientation. It can be used to build self-contained systems, microservices, and serverless functions as well as command-line applications with Java, Kotlin, and Groovy on the JVM or on GraalVM.

The Micronaut project was begun in 2017 by Object Computing, Inc.—the same team that developed the Grails framework, which recently celebrated its 10th anniversary. In late May 2018, the Micronaut project was published under Apache License 2, and Micronaut 1.0 GA was released in October 2018.

Opinionated Framework

Micronaut joins a series of frameworks such as Spring Boot, Grails, Jakarta EE, and MicroProfile that follow an opinionated approach using an annotation-driven programming model that enables fast results. The popularity of these frameworks in the Java community—according to the largest survey ever of Java developers (question 17)—is a testament to the fact that developers overwhelmingly prefer an opinionated framework—that is, one that provides autoconfigurations with reasonable defaults and support for different technologies without requiring developers to put all the pieces together from various components. This programming model distinguishes Micronaut and the others from unopinionated frameworks such as Ratpack, Spark, Vert.x, and Javalin.

The goals for Micronaut were to create a framework that is designed from the ground up for microservices and serverless computing. Micronaut focuses on—and optimizes for—the following aspects of applications for the JVM:

  • Fast application startup time
  • Low runtime memory footprint
  • Minimal use of reflection and proxies
  • Few external dependencies
  • Simple and fast application tests

To meet these goals, the team performed an analysis of the Spring and Grails frameworks and the challenges of using them to develop microservice applications today. Both of those frameworks were launched at a time when monolithic applications predominated, which is why their design was different.

The problem with most of today’s monolithic Java frameworks that provide a large set of out-of-the-box features is that they come with performance and memory-consumption compromises.

One of Micronaut’s primary differences from these frameworks is that it performs dependency injection, aspect-oriented programming (AOP) proxying, and configuration management at compile time. Micronaut processes annotation metadata into ASM-generated code that is used to glue components together. The JVM JIT compiler additionally optimizes the generated bytecode. Other frameworks use reflection to perform these tasks at runtime—generally in the application startup phase—producing runtime annotation metadata and storing the information in memory. Micronaut does not use the Java Reflection API. Instead, it uses a trio of technologies: the Annotation Processor API for Java, the Kapt Kotlin compiler plugin for annotation processors, and the Groovy AST transformations for metaprogramming. Scala is not supported at the time of this writing.

With Micronaut, the startup time and memory consumption of applications are not tied to the size of the codebase. Other reflection-based inversion-of-control frameworks scan the classpath and then load and cache the reflection metadata for each field, method, and constructor. This can create significant overhead. In addition, the more reflection-based microservices are in operation, the more resources they need and the greater the operational costs.

In comparison, Micronaut’s ahead-of-time (AoT) compilation makes it possible to package a minimum Micronaut application in a 10 MB JAR file that can be run with a heap of 10 MB maximum size while enjoying a startup time of about one second. For developers, this benefit is most noticeable during development and the execution of integration tests. Moreover, the fast startup and low memory consumption is a huge advantage for running cloud functions, which is why Micronaut is particularly suitable for the development of serverless applications.

Features

Micronaut has many features that are tailor-made for microservices, including the following:

Reactive streams. Micronaut supports any framework that implements the Reactive Streams standard, including RxJava and Reactor. Web-based nonblocking reactivity is enabled by using Netty. Reactive programming is integral to the design of Micronaut, which consistently supports reactive types to allow efficient use of system resources. For example, Micronaut provides its own service-discover client implementation for Consul that uses Micronaut’s reactive HTTP client—while the majority of existing Consul and Eureka clients provide only blocking access. Also included in the framework are reactive database drivers for SQL databases, such as PostgreSQL, and NoSQL databases, including Neo4j, Redis, MongoDB, and Cassandra.

Cloud native features. Micronaut offers the typical features that you’d expect from a cloud native microservice framework. These include resilience mechanisms (retryables, fallbacks, circuit breakers), service discovery, client-side load balancing, distributed tracing, configuration sharing, and so forth. Default implementations and alternative libraries can be easily integrated into applications by declaring compile-time dependencies from the Micronaut ecosystem and runtime external dependencies. These features are covered in the Micronaut documentation.

Message-driven microservices. Message-driven microservices can be easily implemented with Micronaut’s support for Kafka by using the compile-time AOP annotations @KafkaClient, @KafkaListener, @Topic, @Body, @Header, and @KafkaKey and a few lines of YAML configuration. Equivalent support is planned for RabbitMQ.

Serverless functions. Micronaut provides support for the development, testing, and deployment of serverless functions for AWS Lambda and any framework-as-a-service (FaaS) system, such as OpenFaaS and Fn, that supports running functions in the form of containers. Registered functions can be addressed by using a configured service-discovery service (Consul, Eureka, Kubernetes, Google Cloud Platform, or Amazon Route 53). They can be accessed easily using a @FunctionClient-annotated client and tested in isolation or via an HTTP server.

OpenAPI documentation. API documentation can be done using OpenAPI (or Swagger). Micronaut creates a Swagger 2.x-compliant YAML file at compile time, which is based on regular Micronaut annotations and Javadoc comments. The YAML file can then be added to the application as a static resource and imported into the Swagger user interface.

GraalVM-ready. Due to its AoT compilation, a Micronaut application can be compiled into a native GraalVM image, which further reduces the already-short startup time of a Micronaut application. With a native GraalVM image, startup time drops from about one second to fewer than 100 milliseconds. The memory consumption of about 60 MB for a regular Java app drops to roughly 20 MB for the native process. You can find more information about using GraalVM with Micronaut in the Micronaut Guide.

Getting Started with the Micronaut CLI

Micronaut offers a CLI application. After installing the Micronaut CLI with SDKman, you can generate project setups from the console. You can also use the CLI to create basic (web) applications, command-line applications, serverless functions, and federations (that is, services that share a profile and its features). Features (such as database drivers) can be applied in the CLI, which will add the dependencies to the project and to templates for code and configuration. You can find the complete list of such features online.

Once a project has been created, singleton beans, scheduled jobs, HTTP clients, and controllers as well as WebSocket clients and servers can be scaffolded (see Listing 1).

Listing 1. Micronaut CLI capabilities


$ mn -h
Usage: mn [-hnvVx] [COMMAND]
Micronaut CLI command line interface for generating projects and services.
Commonly used commands are:
  create-app NAME
  create-cli-app NAME
  create-federation NAME --services SERVICE_NAME[,SERVICE_NAME]...
  create-function NAME

Options:
  -h, --help           Show this help message and exit.
  -n, --plain-output   Use plain text instead of ANSI colors and styles.
  -v, --verbose        Create verbose output.
  -V, --version        Print version information and exit.
  -x, --stacktrace     Show full stack trace when exceptions occur.

Commands:
  create-app         Creates an application
  create-cli-app     Creates a command line application
  create-federation  Creates a federation of services
  create-function    Creates a serverless function application
  create-profile     Creates a profile
  help               Prints help information for a specific command
  list-profiles      Lists the available profiles
  profile-info       Display information about a given profile

The Micronaut CLI provides autocompletion and help information in its interactive mode, which provides a smooth user experience.

Hands-on Exercise: Creating a Catalog Service for Books

To illustrate the development of a simple Micronaut application, I will create a catalog service for books. You can find the complete codebase on Github, but I will highlight the important parts here.

I start by running the Micronaut CLI’s create-app command in the console (see Listing 2).

Listing 2. Creating a book-catalog project via the CLI


$ mn create-app catalogservice.book-catalog --features mongo-reactive
| Generating Java project...
| Application created at .../github.com/JonasHavers/book-catalog

[Note: A | at the start of a line indicates screen output from the command. —Ed.]

This command creates a Gradle project (by default) called book-catalog in a folder with the same name. The base package, catalogservice, is created too. The project structure also contains an Application.java class with the main method for starting the application, an application configuration file called application.yml, a logback.xml file for logging via Logback, and a Dockerfile for building an application Docker image that uses Java 8 (as of Micronaut 1.0.3). To make the project ready for Java 11, I changed the base image in the Dockerfile to a JDK 11 image.

The feature mongo-reactive, which is set via the features option, adds the required dependencies to make use of the MongoDB reactive driver. It also configures a default MongoDB connection URI in the application.yml file with default values for the host and port.

To access configuration values in general, such as the database name with the key mongodb-.database, I can use type-safe configuration and validation with Micronaut. That includes referencing and binding configuration values (@Value) at compile time, type-safe mappings to beans by declaring a class annotated with @ConfigurationProperties, validation of properties in Hibernate Validator (for example, @NotNull, @Size, @Min, and @Max), bean factory configurations (@Configuration), and conditional beans (@Requires) with Bean Validation 2.

Now, I can make use of the CLI to scaffold some parts of the application. I create @Singleton beans for the books repository and an HTTP API controller (see Listing 3). There are various annotations for the creation of beans with different scopes (@Singleton, @Prototype, @Refreshable, and so on). These are supported for the different forms of dependency injection with the annotations from JSR 330. Bean injection via the constructor can even be done without the @Inject annotation.

Listing 3. Creating use-case and repository beans and a web controller via the CLI


$ cd book-catalog/
$ mn
| Starting interactive mode 
| Enter a command name to run. Use TAB for completion:
mn> create-bean catalogservice.application.FindBooksUseCase
| Rendered template Bean.java to destination 
src/main/java/catalogservice/application/FindBooksUseCase.java
mn> create-bean catalogservice.adapter.mongodb.MongoBooksRepository
| Rendered template Bean.java to destination 
src/main/java/catalogservice/adapter/mongodb/MongoBooksRepository.java
mn> create-controller catalogservice.adapter.web.BooksApiController
| Rendered template Controller.java to destination 
src/main/java/catalogservice/adapter/web/BooksApiController.java

[Note: Screen ouput that does not start with a | is continued from the previous line. —Ed.]

Controllers take an RFC-6570 URI template for their @Controller and @Get, @Post, and other mappings. I added a MongoClient dependency to the MongoBooksRepository constructor and implemented the port adapter method to fulfill the purpose of the FindBooksUseCase.

Let’s look at an API test. In an integration test inside the BooksApiControllerTest class (see Listing 4), I send a GET request to the API controller with Micronaut’s RxHttpClient, which will access the use case that will then access the database through the repository to return the books data.

Listing 4. Implementing an API test in the BooksApiControllerTest


@Test
public void shouldReturnAllBooks() throws Exception {
  try (EmbeddedServer server = ApplicationContext
    .run(EmbeddedServer.class)) {
    try (RxHttpClient client = RxHttpClient.create(server.getURL())){
      // given
      HttpRequest<?> request = HttpRequest
        .GET("/api/books/");
      // when
      HttpResponse<?> response = client
        .toBlocking()
        .exchange(request, Argument.of(List.class, Book.class));
      // then
      HttpStatus responseStatus = response.status();
      assertEquals(HttpStatus.OK, responseStatus);
      // ...
    }
  }
}

If the MongoDB server is not available on the configured port for the test environment, an embedded MongoDB will be bootstrapped and made available for testing. Alternatively, I can replace the MongoBooksRepository with a test stub. To do this, I need to create another bean that acts as the stub implementation (see Listing 5).

Listing 5. Creating a bean for a books repository test stub


$ mn create-bean catalogservice.adapter.test.StubBooksRepository
| Rendered template Bean.java to destination 
src/main/java/catalogservice/adapter/test/StubBooksRepository.java

In Listing 6, I add the @Replaces annotation to define the bean I want to replace as well as the @Requires annotation with the Environment.TEST value to replace the MongoBooksRepository with the StubBooksRepository when I run the application with the test profile.

Listing 6. Creating a bean as a test stub for the books repository


@Singleton
@Replaces(bean = MongoBooksRepository.class)
@Requires(env = Environment.TEST)
public class StubBooksRepository ... 

I could create other use cases that can be invoked through the API controller and make use of the repository.

To showcase another feature, I want to display the books by using a view. To do this, I can add another controller, BooksViewController (see Listing 7).

Listing 7. Creating the BooksViewController


$ mn create-controller catalogservice.adapter.web.BooksViewController
| Rendered template Controller.java to destination 
src/main/java/catalogservice/adapter/web/BooksViewController.java
| Rendered template ControllerTest.java to destination 
src/test/java/catalogservice/adapter/web/BooksViewControllerTest.java

Although Micronaut is primarily designed for the exchange of pure data (that is, in JSON format), the template engines Thymeleaf, Velocity, and Handlebars are supported for server-side view rendering. The rendering itself is done in the I/O thread pool to avoid blocking the Netty event loop. For this example, I will choose Handlebars.

After creating the controller class with the corresponding CLI command in Listing 7, I edit its source file. I use an instance of Micronaut’s ModelAndView class to render a view template with dynamic data as in Listing 8. You might be familiar with the ModelAndView class from the Spring Web MVC framework. I could have used an HttpResponse or my own data transfer object (DTO) class as a return value, thus requiring me to specify the view name as the value of the @View annotation. To stay in the reactive loop, the return value is encapsulated in a reactive type, in this case an instance of io.reactivex.Single.

Listing 8. Implementing the BooksViewController


@Controller("/")
@RequiredArgsConstructor
public class BooksViewController {
  private final FindBooksUseCase findBooksUseCase;
  @Get
  @View
  public Single<ModelAndView> booksView() {
    return findBooksUseCase.invoke()
      .toList()
      .map(bookList -> {
        Map<String, ?> model = Map.of(
          "books", bookList,
          "numberOfBooks", bookList.size()
        );
        return new ModelAndView("booksView", model);
      });
  }
}

For this example to work, I need to add Project Lombok to the project as well as the Handlebars library (see Listing 9). By default, the Handlebars renderer will process the booksView.hbs template file from the directory src/main/resources/views/.

Listing 9. Adding Project Lombok and Handlebars to build.gradle


compileOnly "org.projectlombok:lombok:1.18.4" // added
annotationProcessor "org.projectlombok:lombok:1.18.4" // added
annotationProcessor "io.micronaut:micronaut-inject-java"
…
runtime "com.github.jknack:handlebars:4.1.2" // added

What’s Next?

I could now add configurations to communicate in a microservices architecture. I could use authentication strategies such as Basic Auth, Session, LDAP, and JSON Web Tokens to secure the service. I could also implement use cases that require WebSockets or server-sent events by using Micronaut’s Event API.

In addition, I could dive into managing and monitoring with restrictable endpoints as well as publishing metrics with the Micrometer library to supported services such as Atlas, Graphite, Prometheus, and StatsD. But I will leave all that to you and your curiosity. In the Micronaut guide, which I have frequently referenced in this article, you will find all the nitty-gritty details for doing these things.

Conclusion

Due to its consistent focus on cloud native computing and the reactive paradigm, Micronaut is particularly suitable for the development of microservices and serverless functions, but it is universally applicable as well. The already-fast startup time of about one second for a Micronaut application can be reduced further by using GraalVM instead of the JVM.

Micronaut is worthy of serious consideration for enterprise applications. If you have an existing Spring Boot application, you can try out Micronaut for Spring to benefit from Micronaut’s AoT compilation.

To learn more about Micronaut, take a look at the official Micronaut guide to familiarize yourself with all the features. There are also official guides for different topics and projects, which are accompanied by code examples. In addition, the examples repository contains a complete pet store application that has been implemented as a federated microservice architecture. Whatever path you choose, I believe you’ll find Micronaut to be a useful lightweight but powerful framework for cloud apps and microservices.

Also in This Issue

Javalin: A Simple, Modern Web Server Framework
Helidon: A Simple Cloud Native Framework
The Proxy Pattern
Loop Unrolling
Quiz Yourself
Size Still Matters
Book Review: Modern Java in Action

Jonas Havers

Jonas Havers (@JonasHavers) is a freelance full-stack software engineer and lecturer on software engineering from Germany. He develops web applications predominantly in ecommerce projects with a mix of Java, Kotlin, Groovy, TypeScript, and JavaScript. He is also an advocate for remote work, and he blogs frequently.

Share this Page