Creating reactive Micronaut and Kotlin services

In this article we will discuss creating a REST service in a “reactive” design. I will give examples of code in Kotlin in two versions: Reactor and Coroutines. I’ll add that almost everything written in the article about reactive implementation of services also applies to SpringBoot.

Micronaut

Micronaut is a JVM framework for creating microservices, it is a JVM framework for creating microservices in Java, Kotlin or Groovy. The creator of the framework is Graeme Rocher. He created the Grails framework and applied much of his knowledge to create Micronaut. Micronaut provides many benefits as a platform.

  • Fast startup time

  • Low memory consumption

  • Efficient dependency injection at compile time

  • Reactive.

Creation of the Micronaut project

There are three ways to create a Micronaut project.

The essence of the service described in the article

In order to evaluate all the features of implementing services on Micronaut, let's look at an example that implements a REST API service with CRUD functionality. We will store the information in an external PostgreSql database. We will assemble the service in two versions: 1) the usual JVM assembly 2) native assembly. Let me remind you that for us who love Java (Kotlin), the option of native assembly is now available.

I chose to demonstrate the “Directories” functionality. At the user level, this is a human-readable key to which certain values ​​are attached. The entries in such a directory can be, for example: “types of documents”: { “Russian Passport”, “Birth Certificate”, etc. }, “Types of currencies”: { “Ruble”, “Dollar”, “Euro”, etc. }. There are many examples of using reference books. The additional value of such implementations is that some data that claims to be “constancy” is not “hard coded” in the system, but is located outside the code with all the system maintenance conveniences that follow from such an implementation. Those. This is a kind of simple two-level structure, where at the top level there is an aggregating record and at the subordinate level there are elements associated with the record.

The code for the full application can be downloaded from the link at the end of the article. I’ll also add that everything we discuss in terms of implementation can also be applied to SpringBoot, since the SpringBoot and Micronaut REST controllers are almost identical.

An educational example that does not pretend to be an ideal implementation of the concept of “pure architecture”; in some places the implementation may seem controversial to some.

The service will also be able to self-document: when the service starts, information on its endpoit will be available, i.e. there will be a visual representation of its essential endpoits in the usual swagger form (OpenApi).

Database (PostgreSql)

The records will be stored in a PostgreSql database. The structure of the records is simple only for demonstrating technologies. In a real system, “directories” are a little more complex in their structure.

Script for creating records in the database:

CREATE TABLE public."dictionary"
(
    id int PRIMARY KEY GENERATED BY DEFAULT AS identity,
    "name" varchar(255)  NOT NULL,
    CONSTRAINT unique_name UNIQUE ("name")
);

CREATE TABLE public.dictionary_value
(
    id int PRIMARY KEY GENERATED BY DEFAULT AS identity,
    code          varchar(255)  NOT NULL,
    value         varchar(255)  NOT NULL,
    dictionary_id int NULL references public."dictionary" (id)
);

Creation of the Micronaut project

To get started, let’s create a Micronaut project with the following features:

Project folder structure

Project folder structure

In implementation we will adhere to the recommendations of “clean architecture”

Model classes

@Serdeable
data class Dictionary(
    val id: Long?,
    @Size(max = 255) val name: String,
    val values: List<DictionaryValue>,
) {
    constructor(id: Long? = null, name: String) : this(id, name, emptyList())
}
@Serdeable
data class DictionaryValue(
    val id: Long = 0L,
    @JsonProperty("parent_id")
    val parentId: Long,
    @Size(max = 80) val code: String,
    @Size(max = 255) val value: String,
)

@Serdeable
data class ShortDictionaryValue(
    @JsonProperty("parent_id")
    val parentId: Long,
    @Size(max = 80) val code: String,
    @Size(max = 255) val value: String,
)

I did not complicate the code and combined the model and dto-shki.

The @Serdeable annotation is needed to allow the type to be serialized or deserialized. You can read about the features of Micronaut serialization, including the advantages of the implementation compared to Jackson Databind here: Micronaut Serialization .

The “ShortDictionaryValue” class is needed for cleaner code when implementing the functionality of adding dictionary value entries. When adding a record, we do not need to use any methods to “hide” the “val id: Long” field that is unnecessary in the context of this operation. And OpenApi in the “swager” representation will be more correct. This is a common technique that can often be found in different implementations.

Service Specifications

Reactor

interface ReactorStorageService<M, K> {
    fun findAll(): Flux<M>
    fun findAll(pageable: Pageable): Mono<Page<M>>
    fun save(obj: M): Mono<M?>
    fun get(id: K): Mono<M?>
    fun update(obj: M): Mono<M>
    fun delete(id: K): Mono<K?>
}
interface ReactorStorageChildrenService<C, K> {
    fun findAllByDictionaryId(id: K): Flux<C>
}

Coroutine

interface CoStorageService<M, K> {
    fun findAll(): Flow<M>
    suspend fun findAll(pageable: Pageable): Page<M>
    suspend fun save(obj: M): M
    suspend fun get(id: K): M?
    suspend fun update(obj: M): M
    suspend fun delete(id: K): K
}
interface CoStorageChildrenService<C, K> {
    suspend fun findAllByDictionaryId(id: K): Flow<C>
}

Please note the difference between Reactor vs Coroutine:

fun noResultFunc(): Mono<Void>
suspend fun noResultFunc()
fun singleItemResultFunc(): Mono<T>
fun singleItemResultFunc(): T?
fun multiItemsResultFunc(): Flux<T>
fun mutliItemsResultFunc(): Flow<T>

Implementation of services

All the implementation specifics are in another module (package).

Model classes associated with a specific DBMS

@Serdeable
@MappedEntity(value = "dictionary")
data class DictionaryDb(
    @GeneratedValue
    @field:Id val id: Long? = null,
    @Size(max = 255) val name: String,
)

@Serdeable
@MappedEntity(value = "dictionary_value")
data class DictionaryValueDb(
    @GeneratedValue
    @field:Id val id: Long? = null,
    @Size(max = 80) val code: String,
    @Size(max = 255) val value: String,
    @MappedProperty(value = "dictionary_id")
    @JsonProperty(value = "dictionary_id")
    val dictionaryId: Long,
)

“Repository” classes

Coroutine:

@R2dbcRepository(dialect = Dialect.POSTGRES)
abstract class CoDictionaryRepository : CoroutinePageableCrudRepository<DictionaryDb, Long> {
    @Query("SELECT * FROM public.dictionary where id = :id;")
    abstract fun findByDictionaryId(id: Long): Flow<DictionaryDb>
}
@R2dbcRepository(dialect = Dialect.POSTGRES)
abstract class CoDictionaryValueRepository : CoroutinePageableCrudRepository<DictionaryValueDb, Long> {

    @Query("SELECT * FROM public.dictionary_value where dictionary_id = :id;")
    abstract fun findAllByDictionaryId(id: Long): Flow<DictionaryValueDb>
}

Reactor

@R2dbcRepository(dialect = Dialect.POSTGRES)
abstract class DictionaryRepository : ReactorPageableRepository<DictionaryDb, Long>
@R2dbcRepository(dialect = Dialect.POSTGRES)
abstract class DictionaryValueRepository : ReactorPageableRepository<DictionaryValueDb, Long> {

    @Query("SELECT * FROM public.dictionary_value where dictionary_id = :id;")
    abstract fun findAllByDictionaryId(id: Long): Flux<DictionaryValueDb>

    @Query("SELECT * FROM public.dictionary_value where dictionary_id = :dictionaryId and code = :code;")
    abstract fun findByCodeAndDictionaryId(
        code: String,
        dictionaryId: Long,
    ): Mono<DictionaryValueDb>
}

Please note that the implementation of these classes is similar to the similar implementation in SpringBoot.

Well, all our repository classes for working with the database are reactive (annotation @R2dbcRepository)

Service implementation

To save the length of the article, I will show only the most important points. All code is available at the link at the end of the article.

@Singleton
class DictionaryService(private val repository: DictionaryRepository) : ReactorStorageService<Dictionary, Long> {

    override fun findAll(pageable: Pageable): Mono<Page<Dictionary>> =
        repository.findAll(pageable).map {
            it.map { itDict ->
                itDict.toResponse()
            }
        }

    override fun findAll(): Flux<Dictionary> = repository.findAll().map { it.toResponse() }
    override fun save(obj: Dictionary): Mono<Dictionary?> = repository.save(obj.toDb()).mapNotNull { it.toResponse() }
    override fun get(id: Long): Mono<Dictionary?> = repository.findById(id).map { it.toResponse() }
    override fun update(obj: Dictionary): Mono<Dictionary> = repository.update(obj.toDb()).map { it.toResponse() }
    override fun delete(id: Long): Mono<Long?> = repository.deleteById(id)
}
@Singleton
class CoDictionaryValueService(private val repository: CoDictionaryValueRepository) :
    CoStorageService<DictionaryValue, Long>, CoStorageChildrenService<DictionaryValue, Long> {

    override fun findAll(): Flow<DictionaryValue> = repository.findAll().map { it.toResponse() }
    override suspend fun findAll(pageable: Pageable): Page<DictionaryValue> = repository.findAll(pageable).map {
        it.toResponse()
    }
    override suspend fun delete(id: Long): Long = repository.deleteById(id).toLong()
    override suspend fun update(obj: DictionaryValue): DictionaryValue = repository.update(obj.toDb()).toResponse()
    override suspend fun get(id: Long): DictionaryValue? = repository.findById(id)?.toResponse()
    override suspend fun save(obj: DictionaryValue): DictionaryValue = repository.save(obj.toDb()).toResponse()

    override suspend fun findAllByDictionaryId(id: Long): Flow<DictionaryValue> {
        return repository.findAllByDictionaryId(id).map { it.toResponse() }
    }
}

Please note the Singleton annotation. Its meaning is similar to the Service annotation in SpringBoot. And there are very few such differences. Those. I once again emphasize the overall strong similarity between Micronaut and SpringBoot. And as a result, the ease of transition if someone also likes Micronaut.

Implementation of controllers

I won’t copy the entire code, but will focus on the most interesting points in my opinion. Pay attention to the annotations. Similar to SpringBoot. Another important point: all variable-parameters of controllers are of the type of interfaces, and not of specific implementations. Controllers know nothing about the specifics of the database or repositories.

@Controller("/api/v1/co-dictionary")
open class CoDictionaryController(
    private val service: CoStorageService<Dictionary, Long>,
    private val dictionaryValueService: CoStorageChildrenService<DictionaryValue, Long>,
) {

    @Get("/list")
    fun findAll(): Flow<Dictionary> = service.findAll()

    @Get("/list-pageable")
    open suspend fun list(@Valid pageable: Pageable): Page<Dictionary> = service.findAll(pageable)

    @Get("/list-with-values")
    fun getAll(): Flow<Dictionary> {
        return service.findAll().mapNotNull(::readDictionary)
    }

    // todo для статьи
    @Get("/stream")
    fun stream(): Flow<Int> =
        flowOf(1,2,3)
            .onEach { delay(700) }

    @Post
    suspend fun save(@NotBlank name: String): HttpResponse<Dictionary> {
        return createDictionary(service.save(Dictionary(name = name)))
    }
    // ...

}
@Controller("/api/v1/dictionary-value")
class DictionaryValueController(private val dictionaryService: ReactorStorageService<DictionaryValue, Long>) {
    @Get("/list-pageable")
    open fun list(@Valid pageable: Pageable): Mono<Page<DictionaryValue>> = dictionaryService.findAll(pageable)

    @Get("/list")
    fun findAll(): Flux<DictionaryValue> = dictionaryService.findAll()

    @Post
    fun save(@NotBlank @Body value: ShortDictionaryValue): Mono<HttpResponse<DictionaryValue>> {
        return dictionaryService.save(value.toResponse()).mapNotNull {
            createDictionaryValue(it!!)
        }
    }

    @Get("/{id}")
    fun get(id: Long): Mono<DictionaryValue?> = dictionaryService.get(id)
// ...
}

Often when implementing the retrieval of such hierarchical data, people argue whether it is necessary to immediately obtain child elements into the general structure? Or receive in lazy mode only when necessary. Both approaches have pros and cons and decisions must be made based on their task context. For example, it is sometimes more convenient for the front to have both the main record and child records at once in order to save effort and not make an additional request to the back. I added this implementation as an example. This is where a very strong and subtle difference between Coroutine and Reactor comes into play. You need to apply reactive stream transformations. Let me remind you that we reactively get both the main record and reactively get the elements associated with this main record and its children. An implementation in Coroutine for example looks like this:

@Get("/list-with-values")
    fun getAll(): Flow<Dictionary> {
        return service.findAll().mapNotNull(::readDictionary)
    }
// ...
private suspend fun readDictionary(dictionary: Dictionary): Dictionary {
        if (dictionary.id == null) return dictionary
        val values = dictionaryValueService.findAllByDictionaryId(dictionary.id).toList()
        if (values.isEmpty()) return dictionary
        return dictionary.copy(
            values = values
        )
    }

Reactivity is preserved, we return Flow. For the version with Coroutine, I also left this “useless” code:

// todo для статьи
    @Get("/stream")
    fun stream(): Flow<Int> =
        flowOf(1,2,3)
            .onEach { delay(700) }

We send the data in a reactive stream and at the same time fall asleep on purpose 🙂 .

Now let's see how it all works together.

Service operation

The assembly will be assembled in two versions:

1) JVM version

2) Native assembly

For native assembly it is convenient to use the corresponding gradle task:

./gradlew dockerBuildNative

You can read about building a docker image for Micronaut here: Building a Docker Image of your Micronaut application

I also posted the native build on docker-hub, which is available for download as “pawga777/micronaut-dictionary:latest”.

You can run the assembled application via docker compose using the following configuration file (docker-compose.yml):

version: '3.5'
services:
  app:
    network_mode: "host"
    environment:
      DB_HOST: localhost
      DB_USERNAME: postgres
      DB_PASSWORD: ZSE4zse4
      DB_NAME: r2-dict-isn
      DB_PORT: 5432
    image: pawga777/micronaut-dictionary:latest

Launch:

Launching the JVM version

Launching the JVM version

Launch "native" versions

Launch of the “native” version

Please note the difference in start time: 1548ms vs 144ms. Impressive? At the same time, a similar version on SpringBoot starts in about 3000ms (Micronaut is fundamentally faster than SpringBoot). In the JVM version of Micronaut, you can still use CRaC technology, which will improve the launch performance if for some reason the native assembly is not suitable. Example with CRaC from Micronaut: Micronaut CRaC .

Swagger (OpenApi)

Swagger (OpenApi)

test request

test request

For simple testing, even Postman is not required since there is a working “swagger”.

Tests

The topic of tests is also extensive. The example uses Kotest. Micronaut can test the work with the database through the technology of “test” containers (Testcontainers). At the same time, an additional small simplification of Testcontainers as “Test Resources” has been added to Micronaut. The key in the phrase above is “added”, that is, both approaches exist. A good example from Micronaut, where they describe these technologies (DBMS H2 and PostgreSQL): REPLACE H2 WITH A REAL DATABASE FOR TESTING. But it seemed suspicious to me that they didn’t have an example in Kotlin. It turned out there is a reason, i.e. Micronaut has an implementation in Kotlin. For the demo, I showed the controller testing approach using one example. We bypass testing repositories with mocks:

@MockBean(CoDictionaryRepository::class)
    fun mockedPostRepository() = mockk<CoDictionaryRepository>()

The testing code for one “handle” looks like this:

given("CoDictionaryController") {
        `when`("test find all") {
            val repository = getMock(coDictionaryRepository)
            coEvery { repository.findAll() }
                .returns(
                    flowOf(
                        DictionaryDb(
                            id = 1,
                            name = "test1",
                        ),
                        DictionaryDb(
                            id = 2,
                            name = "test2",
                        ),
                    )
                )
            val response = client.toBlocking().exchange("/list", Array<Dictionary>::class.java)

            then("should return OK") {
                response.status shouldBe HttpStatus.OK
                val list = response.body()!!
                list.size shouldBe 2
                list[0].name shouldBe "test1"
                list[1].name shouldBe "test2"
            }
        }

We replace only the request for obtaining records directly from the database, leaving the rest of the code (services, controllers) without mocks.

Kotest, by the way, can give the developer some freedom. The example shows the BehaviorSpec approach, which is suitable for lovers of the BDD style. BehaviorSpec allows you to use context, given, when, then. But there are also other specifications: FunSpec, AnnotationSpec, ShouldSpec, FeatureSpec and so on. There are few of them, you can choose a more familiar approach for yourself. AnnotationSpec, for example, is used when migrating from JUnit, allowing you to migrate existing tests (each test as a separate function with the Test annotation:

class AnnotationSpecExample : AnnotationSpec() {

    @BeforeEach
    fun beforeTest() {
        println("Before each test")
    }

    @Test
    fun test1() {
        1 shouldBe 1
    }

    @Test
    fun test2() {
        3 shouldBe 3
    }
}

FunSpec allows you to create tests by calling a function called test with a string argument to describe the test, and then the test itself as a lambda. When in doubt, use this style:

class MyTests : FunSpec({
    test("String length should return the length of the string") {
        "sammy".length shouldBe 5
        "".length shouldBe 0
    }
})

Details about testing in Micronaut here: Micronaut Test

Epilogue

Code for the example described in the article: micronaut-dictionary

Let me remind you that I posted the docker image of the solution on docker-hub.

Thank you to everyone who read my article to the end.

Happy Coding!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *