Skip to main content

Kotlin Micronaut

Introduction

Micronaut is a modern, JVM-based framework designed specifically for building microservices and serverless applications. When paired with Kotlin, it creates a powerful combination that offers both developer productivity and runtime efficiency. Unlike traditional Spring applications, Micronaut avoids reflection at runtime by using ahead-of-time (AOT) compilation, resulting in faster startup times and reduced memory consumption.

In this tutorial, we'll explore how to use Kotlin with Micronaut to build efficient backend services. We'll cover setting up a project, creating RESTful endpoints, dependency injection, database integration, and testing.

Why Micronaut with Kotlin?

Before diving into the code, let's understand why this combination is worth learning:

  • Fast startup time: Perfect for microservices and serverless environments
  • Low memory footprint: Efficient resource utilization
  • Compile-time DI: Catches errors at compile time rather than runtime
  • Native Kotlin support: Coroutines, non-nullable types, and extension functions
  • Cloud-native: Built-in support for cloud deployments
  • Reactive: Non-blocking programming model

Setting Up a Micronaut Kotlin Project

The easiest way to create a Micronaut project is by using the Micronaut CLI or the Micronaut Launch website.

Using Micronaut CLI

First, install the Micronaut CLI:

bash
# Using SDKMAN
sdk install micronaut

# Create a new project
mn create-app com.example.demo --features=kotlin

Project Structure

After creating a project, you'll have a structure similar to this:

├── build.gradle.kts
├── gradle
│ └── wrapper
├── gradlew
├── gradlew.bat
├── micronaut-cli.yml
├── settings.gradle.kts
└── src
├── main
│ ├── kotlin
│ │ └── com
│ │ └── example
│ │ └── demo
│ │ ├── Application.kt
│ │ └── controller/
│ └── resources
│ ├── application.yml
│ └── logback.xml
└── test
└── kotlin
└── com
└── example
└── demo

Let's examine the key files:

  1. Application.kt: The entry point of your application
  2. application.yml: Configuration file
  3. build.gradle.kts: Build configuration

Creating Your First Controller

Let's create a simple REST API. Add a new file src/main/kotlin/com/example/demo/controller/GreetingController.kt:

kotlin
package com.example.demo.controller

import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.PathVariable

@Controller("/greetings")
class GreetingController {

@Get(produces = [MediaType.TEXT_PLAIN])
fun index(): String {
return "Hello, World!"
}

@Get("/{name}", produces = [MediaType.TEXT_PLAIN])
fun greet(@PathVariable name: String): String {
return "Hello, $name!"
}
}

This controller creates two endpoints:

  • GET /greetings - Returns "Hello, World!"
  • GET /greetings/{name} - Returns a greeting with the provided name

Running Your Application

To run the application, execute:

bash
./gradlew run

The application will start on port 8080 by default. You can access:

Output:

Hello, World!
Hello, Kotlin!

Dependency Injection in Micronaut

Micronaut uses compile-time dependency injection instead of the runtime reflection used by frameworks like Spring. Let's see it in action by creating a service layer:

kotlin
package com.example.demo.service

import jakarta.inject.Singleton

@Singleton
class GreetingService {

fun greet(name: String?): String {
return if (name.isNullOrBlank()) {
"Hello, World!"
} else {
"Hello, $name!"
}
}
}

Now let's update our controller to use this service:

kotlin
package com.example.demo.controller

import com.example.demo.service.GreetingService
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.PathVariable

@Controller("/greetings")
class GreetingController(private val greetingService: GreetingService) {

@Get(produces = [MediaType.TEXT_PLAIN])
fun index(): String {
return greetingService.greet(null)
}

@Get("/{name}", produces = [MediaType.TEXT_PLAIN])
fun greet(@PathVariable name: String): String {
return greetingService.greet(name)
}
}

The controller now gets the GreetingService injected through its constructor. Micronaut will handle this at compile time.

Working with Configuration

Micronaut provides powerful configuration capabilities. Let's add some customization to our greeting service:

First, add configuration to src/main/resources/application.yml:

yaml
greeting:
template: "Welcome, %s!"

Now, create a configuration class:

kotlin
package com.example.demo.config

import io.micronaut.context.annotation.ConfigurationProperties

@ConfigurationProperties("greeting")
class GreetingConfig {
var template: String = "Hello, %s!"
}

Update the service to use this configuration:

kotlin
package com.example.demo.service

import com.example.demo.config.GreetingConfig
import jakarta.inject.Singleton

@Singleton
class GreetingService(private val config: GreetingConfig) {

fun greet(name: String?): String {
val nameToUse = name ?: "World"
return config.template.format(nameToUse)
}
}

Now the greeting format will be controlled by the configuration.

Creating a REST API with Data Classes

Kotlin data classes make it simple to create clean models for your API. Let's build a more complex example of a book management API:

kotlin
// src/main/kotlin/com/example/demo/model/Book.kt
package com.example.demo.model

import io.micronaut.core.annotation.Introspected
import io.micronaut.serde.annotation.Serdeable

@Serdeable
@Introspected
data class Book(
val id: Long? = null,
val title: String,
val author: String,
val pages: Int,
val published: Int
)

Now let's create a simple in-memory repository:

kotlin
// src/main/kotlin/com/example/demo/repository/BookRepository.kt
package com.example.demo.repository

import com.example.demo.model.Book
import jakarta.inject.Singleton
import java.util.concurrent.atomic.AtomicLong

@Singleton
class BookRepository {
private val idGenerator = AtomicLong(1)
private val books = mutableMapOf<Long, Book>()

init {
// Add some sample data
save(Book(title = "Kotlin in Action", author = "Dmitry Jemerov", pages = 360, published = 2017))
save(Book(title = "Effective Java", author = "Joshua Bloch", pages = 412, published = 2018))
}

fun findAll(): List<Book> {
return books.values.toList()
}

fun findById(id: Long): Book? {
return books[id]
}

fun save(book: Book): Book {
val id = book.id ?: idGenerator.getAndIncrement()
val savedBook = book.copy(id = id)
books[id] = savedBook
return savedBook
}

fun delete(id: Long): Boolean {
return books.remove(id) != null
}
}

Finally, let's create a controller to expose these operations:

kotlin
// src/main/kotlin/com/example/demo/controller/BookController.kt
package com.example.demo.controller

import com.example.demo.model.Book
import com.example.demo.repository.BookRepository
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.annotation.*
import io.micronaut.http.exceptions.HttpStatusException

@Controller("/books")
class BookController(private val bookRepository: BookRepository) {

@Get
fun getAll(): List<Book> {
return bookRepository.findAll()
}

@Get("/{id}")
fun getById(id: Long): Book {
return bookRepository.findById(id)
?: throw HttpStatusException(HttpStatus.NOT_FOUND, "Book with id $id not found")
}

@Post
fun create(@Body book: Book): HttpResponse<Book> {
val savedBook = bookRepository.save(book)
return HttpResponse.created(savedBook)
}

@Put("/{id}")
fun update(id: Long, @Body book: Book): Book {
if (bookRepository.findById(id) == null) {
throw HttpStatusException(HttpStatus.NOT_FOUND, "Book with id $id not found")
}

return bookRepository.save(book.copy(id = id))
}

@Delete("/{id}")
fun delete(id: Long): HttpResponse<Unit> {
if (bookRepository.delete(id)) {
return HttpResponse.noContent()
}

throw HttpStatusException(HttpStatus.NOT_FOUND, "Book with id $id not found")
}
}

Working with Databases

Micronaut offers excellent database integration. Let's update our book example to use a real database with Micronaut Data.

First, add the necessary dependencies to build.gradle.kts:

kotlin
dependencies {
// ... other dependencies

// JDBC and database driver
implementation("io.micronaut.sql:micronaut-jdbc-hikari")
runtimeOnly("com.h2database:h2")

// Micronaut Data
kapt("io.micronaut.data:micronaut-data-processor")
implementation("io.micronaut.data:micronaut-data-jdbc")
}

Update application.yml for database configuration:

yaml
datasources:
default:
url: jdbc:h2:mem:devDb
driverClassName: org.h2.Driver
username: sa
password: ''
schema-generate: CREATE_DROP
dialect: H2

micronaut:
application:
name: bookApp

Create an entity class:

kotlin
// src/main/kotlin/com/example/demo/entity/BookEntity.kt
package com.example.demo.entity

import io.micronaut.data.annotation.GeneratedValue
import io.micronaut.data.annotation.Id
import io.micronaut.data.annotation.MappedEntity

@MappedEntity("books")
data class BookEntity(
@field:Id
@field:GeneratedValue
val id: Long? = null,
val title: String,
val author: String,
val pages: Int,
val published: Int
)

Create a repository interface:

kotlin
// src/main/kotlin/com/example/demo/repository/BookJdbcRepository.kt
package com.example.demo.repository

import com.example.demo.entity.BookEntity
import io.micronaut.data.annotation.Repository
import io.micronaut.data.jdbc.annotation.JdbcRepository
import io.micronaut.data.model.query.builder.sql.Dialect
import io.micronaut.data.repository.CrudRepository

@JdbcRepository(dialect = Dialect.H2)
interface BookJdbcRepository : CrudRepository<BookEntity, Long> {
fun findByAuthor(author: String): List<BookEntity>
}

Update our BookController to use the database repository:

kotlin
// src/main/kotlin/com/example/demo/controller/BookController.kt
package com.example.demo.controller

import com.example.demo.entity.BookEntity
import com.example.demo.repository.BookJdbcRepository
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.annotation.*
import io.micronaut.http.exceptions.HttpStatusException

@Controller("/books")
class BookController(private val bookRepository: BookJdbcRepository) {

@Get
fun getAll(): List<BookEntity> {
return bookRepository.findAll().toList()
}

@Get("/author/{author}")
fun getByAuthor(author: String): List<BookEntity> {
return bookRepository.findByAuthor(author)
}

@Get("/{id}")
fun getById(id: Long): BookEntity {
return bookRepository.findById(id)
.orElseThrow { HttpStatusException(HttpStatus.NOT_FOUND, "Book with id $id not found") }
}

@Post
fun create(@Body book: BookEntity): HttpResponse<BookEntity> {
val savedBook = bookRepository.save(book)
return HttpResponse.created(savedBook)
}

@Put("/{id}")
fun update(id: Long, @Body book: BookEntity): BookEntity {
if (!bookRepository.existsById(id)) {
throw HttpStatusException(HttpStatus.NOT_FOUND, "Book with id $id not found")
}

return bookRepository.update(book.copy(id = id))
}

@Delete("/{id}")
fun delete(id: Long): HttpResponse<Unit> {
val exists = bookRepository.existsById(id)
if (exists) {
bookRepository.deleteById(id)
return HttpResponse.noContent()
}

throw HttpStatusException(HttpStatus.NOT_FOUND, "Book with id $id not found")
}
}

Testing Micronaut Applications

Micronaut includes great support for testing. Let's create a test for our BookController:

kotlin
// src/test/kotlin/com/example/demo/controller/BookControllerTest.kt
package com.example.demo.controller

import com.example.demo.entity.BookEntity
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import jakarta.inject.Inject

@MicronautTest
class BookControllerTest {

@Inject
@field:Client("/")
lateinit var client: HttpClient

@Test
fun testCreateAndGetBook() {
// Create a new book
val book = BookEntity(
title = "Test Driven Development",
author = "Kent Beck",
pages = 240,
published = 2003
)

val createResponse: HttpResponse<BookEntity> = client.toBlocking()
.exchange(HttpRequest.POST("/books", book), BookEntity::class.java)

assertEquals(HttpStatus.CREATED, createResponse.status)

val createdBook = createResponse.body()!!
assertNotNull(createdBook.id)
assertEquals(book.title, createdBook.title)

// Get the book by ID
val retrievedBook = client.toBlocking()
.retrieve("/books/${createdBook.id}", BookEntity::class.java)

assertEquals(createdBook, retrievedBook)
}

@Test
fun testGetAll() {
// First create some books
val book1 = BookEntity(
title = "Clean Code",
author = "Robert Martin",
pages = 464,
published = 2008
)

val book2 = BookEntity(
title = "The Pragmatic Programmer",
author = "Dave Thomas",
pages = 352,
published = 1999
)

client.toBlocking().exchange(HttpRequest.POST("/books", book1))
client.toBlocking().exchange(HttpRequest.POST("/books", book2))

// Get all books
val books = client.toBlocking().retrieve("/books", Array<BookEntity>::class.java)

assertTrue(books.isNotEmpty())
}
}

Creating a REST API with Coroutines

Micronaut has great support for Kotlin coroutines. Let's create a controller using coroutines:

First, add the coroutines dependency to build.gradle.kts:

kotlin
dependencies {
// ... other dependencies
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("io.micronaut.kotlin:micronaut-kotlin-runtime")
}

Create a coroutine-based controller:

kotlin
// src/main/kotlin/com/example/demo/controller/AsyncBookController.kt
package com.example.demo.controller

import com.example.demo.entity.BookEntity
import com.example.demo.repository.BookJdbcRepository
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.annotation.*
import io.micronaut.http.exceptions.HttpStatusException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking
import java.util.Optional

@Controller("/async-books")
class AsyncBookController(private val bookRepository: BookJdbcRepository) {

@Get
suspend fun getAll(): Flow<BookEntity> {
return bookRepository.findAll().asFlow()
}

@Get("/{id}")
suspend fun getById(id: Long): BookEntity {
return bookRepository.findById(id)
.orElseThrow { HttpStatusException(HttpStatus.NOT_FOUND, "Book not found") }
}

@Post
suspend fun create(@Body book: BookEntity): HttpResponse<BookEntity> {
val saved = bookRepository.save(book)
return HttpResponse.created(saved)
}
}

Note that this is a simplified example, as Micronaut Data doesn't fully support coroutines yet. In a real application, you might want to use the Micronaut reactor support with coroutines or implement your own coroutine-friendly repository.

Summary

In this tutorial, we've explored how to use Kotlin with Micronaut to build efficient microservices. We've covered:

  1. Setting up a Micronaut Kotlin project
  2. Creating controllers and REST endpoints
  3. Dependency injection in Micronaut
  4. Configuration management
  5. Working with databases using Micronaut Data
  6. Testing Micronaut applications
  7. Using Kotlin coroutines with Micronaut (basic introduction)

Micronaut with Kotlin provides a powerful platform for building modern backend services with low memory footprint and fast startup times. This makes it perfect for microservices, serverless applications, and environments where resources are constrained.

Additional Resources

Exercises

  1. Basic Exercise: Create a simple "Todo" API with endpoints for creating, reading, updating, and deleting tasks.

  2. Intermediate Exercise: Implement the Todo API with database persistence using Micronaut Data.

  3. Advanced Exercise: Build a microservice architecture with two services communicating with each other using Micronaut's HTTP client.

  4. Challenge: Implement a reactive API using Micronaut Reactor that streams data to clients and uses coroutines on the server side.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)