Micronaut - From Vision to architecture

As described in the previous blogpost, Micronaut is a framework that provides many different functionalities and tools to build a Microservice application. This blogpost describes, therefore, how all these different functionalities are organized and which patterns are implemented to provide the best experience for a user developing an application.

Overview

The first section, Architectural Patterns, will give an introduction to high-level ideas on how the framework interacts with the application code.

After that, the second section looks into the System Decomposition. This development view describes how the modules are organized.1 In the context of Micronaut, this is important because it explains how the functionalities map to different modules and how Micronaut can be easily extended in the future.

The next important view is the runtime view, which describes how the building blocks interact.2 For Micronaut the most important aspect is the interaction with the user’s application code.

Section 4, the Deployment View, describes what happens when Micronaut gets released and how it is delivered to the end-user.

Finally, the last section covers the Non-functional Requirements and describes which trade-offs were necessary to full-fill them.

Architectural Patterns

As described in the introduction Micronaut highly influences the architecture of the user’s application. Therefore, Micronaut’s internal architecture cannot be seen in isolation.

Micronaut uses a Component-Based Architecture to decompose the design of the framework into modules or logical components. Another pattern extensively used is Inversion of Control which is applied to the internal modules as well as to the application built on top.

  • Component-Based Architecture: offers a way to build software with independent, modular and reusable pieces called components. It provides a higher level of abstraction and divides a problem into sub-problems, each of them associated with a component partition. The choice of this software architectural pattern for Micronaut implies that its development life cycle and software quality get enhanced because its easier for the developers to maintain and improved the vast amount of modules. Additionally, it allows the user to use only the components which are related to the functionality he needs. A more in-depth view of the Micronaut components and its relations is given in the System Decomposition section.3

  • Inversion of Control (IOC): inverts the control flow resulting in the framework itself calling the user’s application code to perform the required tasks.4 It is achieved mainly through the Dependency Injection principle, which is a technique where a central instance (the IoC Container) instantiates an object and supplies its dependencies.5 In Micronaut, the objects instantiated, assembled and managed by the Micronaut IoC container are called beans. Together they form the context of an application. Beans, and the dependencies among them, are reflected in the configuration metadata (normally specified with annotations).6 This pattern influences the whole architecture and is explained in practice in Section 3.

Dependency Injection

System decomposition

Micronaut’s code is built and organized with Gradle. Gradle is a common build tool that allows structuring the code into different modules.7 In total, the code of the micronaut core project is organized in 38 modules. Besides this, the Micronaut framework contains other projects with additional functionality (e.g. Micronaut Data and Micronaut Security).

For narrowing down the scope, this decomposition focuses only on the modules of the core project excluding the tests and the cli project. The following figure shows the remaining 30 modules and the (compile) dependencies on each other. (The direction of the arrow can be read as depends on.)

Micronaut Core's Modules and their Dependencies

The first notable thing is, that the whole project is organized in many different modules and many of them are independent of each other (e.g. messaging and http-server). This has the advantage that they can be used independently as well. For example, an application developer can build an Http-server without having to include the messaging modules.

Furthermore, we can see that inject and core are key modules because the remaining modules depend on them.

By reading the code, we figured out that the core module contains common utility classes and interfaces. This includes, for example, a set of generic annotations, helpers for reactive programming, IO utils and different converters.

The heart of Micronaut is the inject module. It contains the main classes for the Inversion of Control (IoC) pattern based on dependency injection, which has been explained in the previous section. This module manages the application context and all its beans. These beans can be configured with annotations. However, processing those annotations is part of a different module (java-inject or groovy-inject). This is necessary because, in contrast to other frameworks, Micronaut uses ahead-of-time (AOT) compilation to process those annotations (see Section Non-functional Requirements).

Having them as separate modules allows registering them as annotation processors, which is a way to extend the java compiler to generate classes at compilation time.8 It’s important to remark that the dependency injection mechanism is not only used by the user’s application code. All other Micronaut modules depend on inject to supply their beans to the application.

One examples is the module aop, which implements Micronauts compiler-based Aspect-Oriented Programming (AOP). AOP allows controlling the program flow with meta-programming and annotations9. The runtime module which is responsible for starting and managing the life cycle of the application.

For allowing the user to build an Http server or client, the modules http-server and http-client (and all their transitive dependencies) are required. The modules function-web and function-client contain the APIs to write serverless functions. The core project only contains a generic implementation, the integration with different cloud-providers is implemented via external modules (e.g. Micronaut AWS).

All Micronaut core modules together depend (transitively) on around 40 libraries. Some of those dependencies are: SL4J for logging, RxJava for reactive programming, Jackson Databind for mapping objects to JSON, SnakeYAML for parsing YAML files, Caffeine for caching and ASM for modifying binary Java classes. To conclude, we can say that Micronaut is not using many 3rd party libraries and the ones it is using are well-known and widely used in the Java community.

Interactions of Micronaut’s IoC

As described before, the Micronaut framework includes many different functionalities and therefore the interaction of the components highly depends on the user’s configuration. Additionally, Micronaut follows the Convention over Configuration principle and has an extensive predefined behavior that is hidden from the user’s application code. Executing the Hello-World Example which only contains a simple REST interface already results in many complex interactions. Nevertheless, to give an idea of the IoC implementation in practice, we take the Hello-World example and describe a few interactions when executing it.

One key functionality of Micronaut is the ahead-of-time compilation. By adding the module java inject to the compiler, the annotations are processed at compile-time. The compiler generates classes based on the annotations with the definitions of the beans. Afterward, Micronaut can create beans and read their metadata from the generated classes. Micronaut does not need to use the slow reflection API at run-time.10

Hello World Example: classes generated during compilation

In the startup of the application the application context with all beans is created (See figure for more details). During this phase, all meta-data of the annotations are evaluated and are then wired together. For example, the IoC Container injects all the REST controller beans into the Router bean. Based on them the router can create a mapping table for all defined routes. After creating the application context, the Netty web server is started.

Startup phase in the Hello World example

When a client sends an HTTP request to the endpoint /hello. This call is processed by the Netty server which triggers a handler in the netty-http-server module. This handler uses the Router bean to determine the corresponding controller which, in this case, is the HelloController bean. It calls the related method and forwards the result (‘‘Hello World!”) back to the Netty server which sends it back on the HTTP channel.

Deployment view

As stated in Rozanski and Woods, the Deployment view focuses on aspects of the system that are important after the system has been tested and is ready to go into live operation.1 Going into live operation means for Micronaut either being executed as part of a user’s application or the distribution of Micronaut’s framework to the user.

In the first case, the execution environment highly depends on the user’s configuration. He can execute it, for example, as an Android application, a serverless function or a microservice executed in a cloud environment. Furthermore, Micronaut can be integrated with countless third party services. For example, different databases (Micronaut Data), messaging infrastructure (Micronaut RabbitMQ) and authentication providers (Micronaut Security). All these options can lead to a high number of different execution environments.

Therefore, we are focusing on the second case. When building Micronaut two kinds of artifacts are generated.11 On one hand, we have the classical Java libraries which can be added as dependencies to an application. These artifacts are deployed to Maven Central and can be downloaded there by the users, either manually or automatically with a build system like Maven or Gradle. It’s important to mention, that for every module described in System Decomposition a separate jar library is created. This enhances the component-based architecture (see Architectural Patterns) and allows the user to only download the artifacts he needs.

On the other hand, we have the cli application which is the recommended way to create new projects.12 This application is deployed on three different ways depending on the running platform:

  1. Using SDKMAN for Unix-like systems.
  2. Using Homebrew for macOS.
  3. Install through a binary on Windows.

A graphical overview of the deployment view in Micronaut

The figure above resumes the main components from the deployment view. First of all, Micronaut is a JVM framework, thus it requires Java and its development kit in the 8 or 11 version. The development team also gives support for other JVM languages such as Groovy or Kotlin. As explained in System Decomposition, Micronaut requires some 3rd party libraries. These libraries are automatically downloaded as transitive dependencies when a build system is used.

Micronaut can run on multiple platforms (Windows, macOS, and Linux) and it doesn’t require any higher system requirement than the JDK 8.

Non-functional requirements

Non-functional requirements or also called quality properties are requirements that “do not directly mandate functionality but still have a significant impact on the architecture”.1 Examples of non-functional requirements include the performance, security, usability, and scalability of a system.13

Micronaut’s non-functional requirements are focused on performance, specifically fast startup times (timing behavior) and reduced memory footprint (resource utilization).

Traditionally, frameworks do things such as annotation processing, reading byte code14, invoking endpoints, and performing data binding using reflection and proxies.15 Reflection happens at runtime and its use has multiple disadvantages.16

First of all, reflection leads to large memory consumption.14 Since reflection is slow, frameworks create a reflection data cache to limit the number of expensive reflection calls. As there is no standard way of defining reflection caches each framework defines its own, causing a lot of redundant data being cached. To make matters worse, reflection data is not garbage collected until the application is low on memory. Secondly, the more beans an application has, the more (slow) reflection calls are needed to analyze the classes, which increases the startup time of an application.16 17

Micronaut eliminates the need for reflection and proxies by using the already mentioned ahead-of-time (AOT) compilation. AOT compilation essentially means that more is done at compile-time and less at runtime. With the classes generated at compile-time, there is no need for using reflection at run-time.15

Using AOT compilation does come with its own set of drawbacks. Firstly, it leads to longer compilation times, as everything normally done at runtime using reflection is now done at compile-time.18 Longer compilation times might slow down development and affect testability, as developers now have to wait longer before they can run or test their code. Additionally, in order to achieve AOT compilation, Micronaut had to create its own abstraction over the Java annotation processor API15 and implement its own Dependency Injection (see Architectural Pattern) and AOP implementations.19 All of this is code has to be maintained, which might affect Micronaut’s future maintainability.

  1. Nick Rozanski and Eoin Woods. Software Systems Architecture: Working with Stakeholders Using Viewpoints and Perspectives. Addison-Wesley, 2012, 2nd edition.  2 3

  2. Peter Hruschka and Gernot Starke. arc42: Effective, lean and pragmatic architecture documentation and communication. https://docs.arc42.org/home/ 

  3. Rainer Niekamp. Software Component Architecture. http://congress.cimne.upc.edu/cfsi/frontal/doc/ppt/11.pdf 

  4. Martin Fowler. Inversion of Control. 2005 https://martinfowler.com/bliki/InversionOfControl.html 

  5. Martin Fowler. Inversion of Control Containers and the Dependency Injection pattern. 2004 https://martinfowler.com/articles/injection.html 

  6. Micronaut Documentation. Inversion of Control. https://docs.micronaut.io/latest/guide/index.html#ioc 

  7. Gradle Inc. Gradle User Manual. https://docs.gradle.org/current/userguide/userguide.html 

  8. Hannes Dorfmann. Annotation Processing. 2015 Annotation Processing 

  9. Martin Fowler. Domain-Oriented Observability - Aspect-Oriented Programming. https://martinfowler.com/articles/domain-oriented-observability.html#Aspect-orientedProgramming 

  10. Oracle. The Java™ Tutorials - The Reflection API https://docs.oracle.com/javase/tutorial/reflect/index.html 

  11. Graeme Rocher. Micronaut Core Release Process https://github.com/micronaut-projects/micronaut-core/blob/1.3.x/RELEASE.adoc 

  12. Micronaut Documentation. Micronaut CLI. https://docs.micronaut.io/latest/guide/index.html#cli 

  13. Calidad Software. ISO2500 Software and Data Quality https://iso25000.com/index.php/en/iso-25000-standards/iso-25010 

  14. Graeme Rocher. Introduction to Micronaut. 2018 https://objectcomputing.com/files/8415/4220/9027/18-11-14-Intro-Micronaut-Webinar-slide-deck.pdf  2

  15. Object Computing. Micronaut 1.0 RC and the power of Ahead-Of-Time Compilation https://objectcomputing.com/news/2018/09/30/micronaut-1-rc1  2 3

  16. Graeme Rocher. Introduction to Micronaut. GOTO 2019, 2019 https://www.youtube.com/watch?v=RtjSqRZ_md4  2

  17. Graeme Rocher. Introduction to Micronaut. JBNCConf 2019, 2019 https://github.com/micronaut-projects/presentations/blob/master/jbcnconf-2019.md 

  18. Moritz Kammerer. Microservices with Micronaut. Cloud-Native Night, 2019, https://www.slideshare.net/QAware/microservices-with-micronaut 

  19. Graeme Rocher. Micronaut Deep Dive. Devoxx, 2019 https://www.youtube.com/watch?v=S5yfTfPeue8 

Micronaut