Groovy / Gradle

Kafka and the Micronaut Framework - Event-Driven Applications

Use Kafka to communicate between your Micronaut applications.

Burt Beckwith
On this guide
In this section

Getting Started

In this guide, we will create a Micronaut application written in Groovy.

In this guide, we will create two microservices that will use Kafka to communicate with each other in an asynchronous and decoupled way.

What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 17 or greater installed with JAVA_HOME configured appropriately

  • Docker and Docker Compose installed if you will be running Kafka in Docker, and for running tests.

Solution

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

Writing the application

Let’s describe the microservices you will build through the guide.

  • books - It returns a list of books. It uses a domain consisting of a book name and ISBN. It also publishes a message in Kafka every time a book is accessed.

  • analytics - It connects to Kafka to update the analytics for every book (a counter). It also exposes an endpoint to get the analytics.

Books Microservice

Create the books microservice using the Micronaut Command Line Interface or with Micronaut Launch.

mn create-app --features=kafka,reactor,graalvm,serialization-jackson example.micronaut.books --build=gradle --lang=groovy
Note
If you don’t specify the --build argument, Gradle with the Kotlin DSL is used as the build tool.
If you don’t specify the --lang argument, Java is used as the language.
If you don’t specify the --test argument, JUnit is used for Java and Kotlin, and Spock is used for Groovy.

If you use Micronaut Launch, select Micronaut Application as application type and add the kafka, reactor, graalvm, and serialization-jackson features.

The previous command creates a directory named books and a Micronaut application inside it with default package example.micronaut.

In addition to the dependencies added by the above features, we also need a test dependency for the Awaitility library:

build.gradle
testImplementation("org.awaitility:awaitility:@awaitilityVersion@")

Create a Book POJO:

books/groovy/src/main/groovy/example/micronaut/Book.groovy
package example.micronaut

import groovy.transform.Canonical
import groovy.transform.CompileStatic
import io.micronaut.serde.annotation.Serdeable

@Canonical
@CompileStatic
@Serdeable
class Book {
    String isbn
    String name
}

To keep this guide simple there is no database persistence - BookService keeps the list of books in memory:

books/groovy/src/main/groovy/example/micronaut/BookService.groovy
package example.micronaut

import groovy.transform.CompileStatic

import jakarta.annotation.PostConstruct
import jakarta.inject.Singleton

@CompileStatic
@Singleton
class BookService {

    private final List<Book> bookStore = []

    @PostConstruct
    void init() {
        bookStore << new Book('1491950358', 'Building Microservices')
        bookStore << new Book('1680502395', 'Release It!')
        bookStore << new Book('0321601912', 'Continuous Delivery')
    }

    List<Book> listAll() {
        bookStore
    }

    Optional<Book> findByIsbn(String isbn) {
        Optional.ofNullable(bookStore.find { it.isbn == isbn })
    }
}

Create a BookController class to handle incoming HTTP requests to the books microservice:

books/groovy/src/main/groovy/example/micronaut/BookController.groovy

Analytics Microservice

Create the analytics microservice using the Micronaut Command Line Interface or with Micronaut Launch.

mn create-app --features=kafka,graalvm,serialization-jackson example.micronaut.analytics --build=gradle --lang=groovy
Note
If you don’t specify the --build argument, Gradle with the Kotlin DSL is used as the build tool.
If you don’t specify the --lang argument, Java is used as the language.
If you don’t specify the --test argument, JUnit is used for Java and Kotlin, and Spock is used for Groovy.

If you use Micronaut Launch, select Micronaut Application as application type and add the kafka and graalvm features.

Create a Book POJO:

analytics/groovy/src/main/groovy/example/micronaut/Book.groovy
package example.micronaut

import groovy.transform.Canonical
import groovy.transform.CompileStatic
import io.micronaut.serde.annotation.Serdeable

@Canonical
@CompileStatic
@Serdeable
class Book {
    String isbn
    String name
}
Note
This Book POJO is the same as the one in the books microservice. In a real application this would be in a shared library but to keep things simple we’ll just duplicate it.

Create a BookAnalytics POJO:

analytics/groovy/src/main/groovy/example/micronaut/BookAnalytics.groovy
package example.micronaut

import groovy.transform.Canonical
import groovy.transform.CompileStatic
import io.micronaut.serde.annotation.Serdeable

@Canonical
@CompileStatic
@Serdeable
class BookAnalytics {
    String bookIsbn
    long count
}

To keep this guide simple there is no database persistence - AnalyticsService keeps book analytics in memory:

analytics/groovy/src/main/groovy/example/micronaut/AnalyticsService.groovy

Write a test for AnalyticsService:

analytics/groovy/src/test/groovy/example/micronaut/AnalyticsServiceSpec.groovy
package example.micronaut

import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.Specification

import jakarta.inject.Inject

@MicronautTest
class AnalyticsServiceSpec extends Specification {

    @Inject
    AnalyticsService analyticsService

    void 'test update book analytics and get analytics'() {
        given:
        Book b1 = new Book('1491950358', 'Building Microservices')
        Book b2 = new Book('1680502395', 'Release It!')

        when:
        analyticsService.updateBookAnalytics b1
        analyticsService.updateBookAnalytics b1
        analyticsService.updateBookAnalytics b1
        analyticsService.updateBookAnalytics b2

        List<BookAnalytics> analytics = analyticsService.listAnalytics()

        then:
        2 == analytics.size()
        3 == findBookAnalytics(b1, analytics).count
        1 == findBookAnalytics(b2, analytics).count
    }

    private BookAnalytics findBookAnalytics(Book b, List<BookAnalytics> analytics) {
        BookAnalytics bookAnalytics = analytics.find { it.bookIsbn == b.isbn }
        if (!bookAnalytics) {
            throw new RuntimeException('Book not found')
        }
        bookAnalytics
    }
}

Create a Controller to expose the analytics:

analytics/groovy/src/main/groovy/example/micronaut/AnalyticsController.groovy
Note

The application doesn’t expose the method updateBookAnalytics created in AnalyticsService. This method will be invoked when reading messages from Kafka.

To run the tests:

analytics
./gradlew test

Modify the Application class to use dev as a default environment:

The Micronaut framework supports the concept of one or many default environments. A default environment is one that is only applied if no other environments are explicitly specified or deduced.

analytics/groovy/src/main/groovy/example/micronaut/Application.groovy
package example.micronaut

import groovy.transform.CompileStatic
import io.micronaut.runtime.Micronaut

import static io.micronaut.context.env.Environment.DEVELOPMENT

@CompileStatic
class Application {

    static void main(String[] args) {
        Micronaut.build(args)
                .mainClass(Application)
                .defaultEnvironments(DEVELOPMENT)
                .start()
    }
}

Create src/main/resources/application-dev.properties. The Micronaut framework applies this configuration file only for the dev environment.

analytics/groovy/src/main/resources/application-dev.properties

Running the application

Start the books microservice:

books
./gradlew run
16:35:55.614 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 576ms. Server Running: http://localhost:8080

Start the analytics microservice:

analytics
./gradlew run
16:35:55.614 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 623ms. Server Running: http://localhost:8081

You can use curl to test the application:

curl http://localhost:8080/books
[{"isbn":"1491950358","name":"Building Microservices"},{"isbn":"1680502395","name":"Release It!"},{"isbn":"0321601912","name":"Continuous Delivery"}]
curl http://localhost:8080/books/1491950358
{"isbn":"1491950358","name":"Building Microservices"}
curl http://localhost:8081/analytics
[]

Note that getting the analytics returns an empty list because the applications are not communicating with each other (yet).

Test Resources

When the application is started locally, either under test or while running locally, resolution of the property kafka.bootstrap.servers is detected and the Test Resources service will start a local Kafka docker container, and inject the properties required to use this as the broker.

When running under production, you should replace this property with the location of your production Kafka instance via an environment variable.

KAFKA_BOOTSTRAP_SERVERS=production-server:9092

For more information, see the Kafka section of the Test Resources documentation.

Kafka and the Micronaut Framework

Install Kafka

A fast way to start using Kafka is via Docker. Create this docker-compose.yml file:

docker/docker-compose.yml

Start Zookeeper and Kafka (use CTRL-C to stop both):

docker-compose up

Books Microservice

The generated code will use the Test Resources plugin to start a local Kafka broker inside Docker, and configure the connection URL.

Create Kafka client (producer)

Let’s create an interface to send messages to Kafka. The Micronaut framework will implement the interface at compilation time:

books/groovy/src/main/groovy/example/micronaut/AnalyticsClient.groovy

Create Tests

We could use mocks to test the message sending logic between BookController, AnalyticsFilter, and AnalyticsClient, but it’s more realistic to use a running Kafka broker. This is why Test Resources are used to run Kafka inside a Docker container.

Write a test for BookController to verify the interaction with AnalyticsService:

books/groovy/src/test/groovy/example/micronaut/BookControllerSpec.groovy

Send Analytics information automatically

Sending a message to Kafka is as simple as injecting AnalyticsClient and calling the updateAnalytics method. The goal is to do it automatically every time a book is returned, i.e., every time there is a call to http://localhost:8080/books/{isbn}. To achieve this we will create an Http Server Filter. Create the AnalyticsFilter class:

books/groovy/src/main/groovy/example/micronaut/AnalyticsFilter.groovy

Analytics Microservice

Create Kafka consumer

Create a new class to act as a consumer of the messages sent to Kafka by the books microservice. The Micronaut framework will implement logic to invoke the consumer at compile time. Create the AnalyticsListener class:

analytics/groovy/src/main/groovy/example/micronaut/AnalyticsListener.groovy

Running the application

Start the books microservice:

books
./gradlew run
16:35:55.614 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 576ms. Server Running: http://localhost:8080

Execute a curl request to get one book:

curl http://localhost:8080/books/1491950358
{"isbn":"1491950358","name":"Building Microservices"}

Start the analytics microservice:

analytics
./gradlew run
16:35:55.614 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 623ms. Server Running: http://localhost:8081

The application will consume and process the message automatically after startup.

Now, use curl to see the analytics:

curl http://localhost:8081/analytics
[{"bookIsbn":"1491950358","count":1}]

Update the curl command to the books microservice to retrieve other books and repeat the invocations, then re-run the curl command to the analytics microservice to see that the counts increase.

Next Steps

Read more about Kafka support in Micronaut framework.

Read more about Test Resources in Micronaut.

License

Note
All guides are released with an Apache License 2.0 for the code and a Creative Commons Attribution 4.0 license for the writing and media (images).