For my upcoming article, I decided to dive into the Micronaut world. And my experience has been amazing. I can tell you right off the bat, that Micronaut is pretty fast, extremely efficient, has record breaking startup times and is can be pretty complicated. (complicated in this case is in the eye of the beholder).


In this bite, my goal is to let you know my process into learning Micronaut and hopefully make you avoid the bitter wrong turns I've made into learning this.


Micronaut is not Spring!

As much as we like to think of Micronaut as an alternative to Spring, it is really not technically speaking, the same. Spring is a framework that many people can use on the basis of "magic". What I mean to say is that you can get it to work using excessive resources and poor understanding of the framework. Micronaut, can also be that, but in my experience you do need to know a lot more in depth what you are doing, even before you get it to work. Stackoverflow may not help you as much and an extensive read of the documentation is warranted. It is also warranted for Spring anyways, but Spring forgives a lot of things especially excessive usage. This of course says nothing about which framework I prefer. I think both are equally great!


Choosing Gradle vs Maven

The Micronaut team has a preference for Gradle. They have a whole page dedicated to explaining this on MICRONAUT 2.0 MILESTONE 2: MASSIVE MAVEN IMPROVEMENTS. I wasn't aware of this when I started out developing my project. It was only after I went a bit ahead with this that I realised this. And the way I realised this was when I kept seeing this blast from the past in my logs:

I could't find another annotation processor that works well with Maven other than the already deprecated Kapt. This deprecated annotation processor is still what is being used when generating a new Micronaut project in maven with command mn. The only alternative I found it KSP(Kotlin Symbol Processing API). But of course, this is only fully documented to be used with gradle. The fact that we even need to be concerned with an annotation processor is already an indication of what we need to think about then using Micronaut. In Spring we can also do this, but what comes out of the box is a fully fledged annotation processor that works very well underwater. But probably does much more than it should. In Micronaut, we have no option. We need to specify the annotation processors.


Creating a micronaut project

Creating a micronaut project is just as easy as creating a spring project. You also have a page dedicated to create the initial package and you can then choose your libraries. This page is called Micronaut Launch. You can also create a project via the command line with the mn command. I created all my subprojects using this command line and ended up having a gigantic pom.xml file. The reason for this is that, as I mentioned before, we need to specify the annotation processors. In my case they have to be specified in the kotlin maven plugin and it goes something like this:

<plugin>
    <artifactId>kotlin-maven-plugin</artifactId>
    <groupId>org.jetbrains.kotlin</groupId>
    <version>${kotlin.version}</version>
    <configuration>
        <jvmTarget>${java.version}</jvmTarget>
        <compilerPlugins>
            <plugin>all-open</plugin>
        </compilerPlugins>
        <pluginOptions>
            <option>all-open:annotation=io.micronaut.aop.Around</option>
        </pluginOptions>
    </configuration>
    <executions>
        <execution>
            <id>kapt</id>
            <goals>
                <goal>kapt</goal>
            </goals>
            <configuration>
                <sourceDirs>
                    <sourceDir>${project.basedir}/src/main/kotlin</sourceDir>
                </sourceDirs>
                <annotationProcessorPaths combine.self="override">
                    (all annotation processors needed)
                </annotationProcessorPaths>
                <annotationProcessorArgs>
                    (all annotation processor arguments needed)
                </annotationProcessorArgs>
            </configuration>
        </execution>
        <execution>
            <id>compile</id>
            <phase>compile</phase>
            <goals>
                <goal>compile</goal>
            </goals>
            <configuration>
                <sourceDirs>
                    <source>src/main/kotlin</source>
                    <source>target/generated-sources/kaptKotlin</source>
                </sourceDirs>
            </configuration>
        </execution>
        <execution>
            <id>test-kapt</id>
            <goals>
                <goal>test-kapt</goal>
            </goals>
            <configuration>
                <sourceDirs>
                    <sourceDir>${project.basedir}/src/test/kotlin</sourceDir>
                </sourceDirs>
                <annotationProcessorPaths combine.self="override">
                  (all annotation processors needed)
                </annotationProcessorPaths>
            </configuration>
        </execution>
        <execution>
            <id>test-compile</id>
            <phase>test-compile</phase>
            <goals>
                <goal>test-compile</goal>
            </goals>
            <configuration>
                <sourceDirs>
                    <source>target/generated-sources/kapt/test</source>
                    <source>src/test/kotlin</source>
                </sourceDirs>
            </configuration>
        </execution>
    </executions>
    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-allopen</artifactId>
            <version>${kotlin.version}</version>
        </dependency>
    </dependencies>
</plugin>

The reason I emphasise the configuration of this plugin, is because it is very, very important to understand even in the very lightest of ways, what we got here after the project generation. For our code we need to indicate which annotation processors are needed and their eventual respective arguments. These processors make sure that important annotations we need for injection get recognised and processed at compile time. Unlike Spring, some or all of this processing, gets translated into classes. This is part of the reason why Micronaut is so great and why its startup is so much faster than spring-boot with a default configuration.


Creating DAO's, Model, Repository and the rest of the Data Layer

In the project we are looking at, the data layer doesn't have a lot of differences when we compare it with Spring. A very extensive investigation over it isn't really the goal of this bite, but we can still admire and compare. Let's have a look at one particular data model to handle recepts:

@MappedEntity("receipt", namingStrategy = NamingStrategies.UnderScoreSeparatedLowerCase::class)
data class Receipt(
    @field: Id
    @field: AutoPopulated
    val id: UUID? = null,
    val reference: UUID = UUID.randomUUID(),
    @field:DateCreated
    val createdAt: LocalDateTime? = LocalDateTime.now(),
    @field: Relation(value = Relation.Kind.ONE_TO_ONE, cascade = [Relation.Cascade.PERSIST])
    val ticketReservation: TicketReservation?= null
)

@Singleton
@R2dbcRepository(dialect = Dialect.POSTGRES)
interface ReceiptRepository : CoroutineCrudRepository<Receipt, UUID>,
    CoroutineJpaSpecificationExecutor<Receipt>

In this project I'm using coroutines, but that is a different topic. However, it is always important to refresh our memory in terms of what coroutines are. They are, long story short, a segmentation of a thread into different parallel processes. Having this in mind, we can observe that not that much changes. We do seem to be able to specify more things per entity/repository much more easily if we compare this to Spring. If you don't know how the Spring syntax to make entities and map data code to database queries, then it really doesn't matter that much. It's only important to understand that we are mapping this code to a table receipt and we are using repository ReceiptRepository to perform queries against it.

According to MVC models, we also have a service layer which in Spring is usually comprised with classes annotated with the stereotype @Service. The way I found to do this was to use @Singleton, which basically creates a @Singleton bean (note that at this point, the code presented moves a bit away from standards as it is still WIP(Work in Progress):

@Singleton
class ReservationsService(
    private val receiptRepository: ReceiptRepository,
    private val redisClient: RedisClient,
    val pubSubCommands: RedisPubSubAsyncCommands<String, String>
) {

    init {
        val statefulRedisPubSubConnection = redisClient.connectPubSub()
        statefulRedisPubSubConnection.addListener(Listener())
        val redisPubSubAsyncCommands = statefulRedisPubSubConnection.async()
        redisPubSubAsyncCommands.subscribe("channel1")
        redisClient.connectPubSub().async().publish("channel1", "test")
    }

    @OptIn(DelicateCoroutinesApi::class)
    suspend fun createTicket(ticketDto: @Valid TicketDto) = GlobalScope.launch {
        pubSubCommands.publish("channel1", "test")
        receiptRepository.save(Receipt()).toDto
    }

    fun getAll(): Flow<Receipt> = receiptRepository.findAll()
}

What I personally like about using @Singleton to define a bean is that it makes it clear that it is just a bean. In Spring, we can also inject @Component which is functionally no different than @Service. As you can see I'm injecting RedisClient and the ReceiptRepository. Im also injecting a RedisPubSubAsyncCommands bean. This is a manually made bean. It is created by using instruction redisClient.connectPubSub().async().

What I immediately found out is that making a bean out of such instruction is not as easy as just using a @Bean annotation in a method inside a @Configuration annotated class, such as is the case with Spring. This is a Micronaut's world! And so, to create a bean manually like this, we need to do something special.


Creating a bean manually

In order to create a bean manually, I did fell into the trap of thinking that Micronaut should work with what I saw in many websites out of the box with the current code:

@Factory
class RedisBeanFactory {
    @Singleton
    fun pubSubCommands(redisClient: RedisClient): RedisPubSubAsyncCommands<String, String> =
        redisClient.connectPubSub().async()
}

Essentially, instead of a configuration as in Spring, we create instead a Bean Factory. Seems like pure semantics, but there is a small but very crucial difference. In order to create these beans, we need to activate something called Bean Introspection. This is not activated by default. The reason should be more evident at this point. Since only now we found out that we need it, then we activate it as opposed to having it readily activated and causing overhead for no reason. The way to do this is what they have described in their page. We need this annotation processor:

<annotationProcessorPath>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-inject-java</artifactId>
    <version>${micronaut.version}</version>
</annotationProcessorPath>

And maybe not that obvious when using so many dependencies, we need the core dependency:

<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-core</artifactId>
    <scope>runtime</scope>
</dependency>

This is a runtime dependency. If you don't know what that is, this is a dependency only used during the execution runtime of the application. It is only available in the runtime and test classpaths.


Creating the Controller

The controller creation is basically done by using standard annotations. If you are already familiar with the MVC (model view controller) pattern then the syntax should be familiar to you:

@Controller("/")
open class WelcomeController(
    private val receiptRepository: ReceiptRepository
) {
    @Get(value = "/", produces = [MediaType.APPLICATION_JSON])
    open suspend fun getWelcomeMessage() = "message" to "Welcome to the Receipt service"

    @Get(value = "/test", produces = [MediaType.APPLICATION_JSON])
    open fun getAllParkingReservations(): Flow<ReceiptDto> {
        return receiptRepository.findAll().map { it.toDto }
    }
}

Running code with an IDE

I guess IDE's are getting better and by the time you read this, you may not find this issue, but I did find problems running code and making changes via the IDE. If you build anything with Micronaut, you'll see an unusual amount of classes being generated in the target folder. This is how Micronaut stands out from other frameworks. The Injection process differs greatly from Spring. In Micronaut, injection happens at compile-time. This is probably the reason why I faced issues with Intellij. The only way I found out to overcome issues was simply to run mvn clean install everytime I suspected the compilation dit not went well. It happened at random occasions so I'm just assuming that it has something to do with compile-time. Although this was a hurdle at times, the gain actually greatly surpasses this, because this compile-time philosophy is one of the strong reasons why Micronaut can be much more performant than other frameworks.


It took me a while to understand all of this and I did ran into several traps just to get the beans to work. These are the basics of all you need to know to get a simple application running. All with a little bit more than the basics. Current code will suffer changes as I go along, but the essentials are there already. I highly recommend you to thoroughly read through both Micronaut and Spring API's documentation before doing anything in production. I get the trial and error process idea. It is extremely important to also do that, but in many ways you just can't go further without an in depth understanding of the framework. Especially if you are trying to understand Bean Introspection. If you've been wondering on which project I'm performing these changes, this is the one: https://github.com/jesperancinha/buy-odd-yucca-concert/tree/master/buy-oyc-api-service. I hope that with all of this, I was able to help you speed up your process of learning Micronaut. I still have a long way to go, but this is the status so far.