Apache Kafka is a distributed stream processing platform that can be used for a range of messaging requirements in addition to stream processing and real-time data handling.
Micronaut features dedicated support for defining both Kafka Producer and Consumer instances. Micronaut applications built with Kafka can be deployed with or without the presence of an HTTP server.
With Micronaut’s efficient compile-time AOP and cloud native features, writing efficient Kafka consumer applications that use very little resources is a breeze.
2 Release History
For this project, you can find a list of releases (with release notes) here:
Micronaut Kafka 6.0 is a significant major version which updates the framework, Kafka client, and Java baselines and includes a number of listener, client, and administration enhancements.
Micronaut 5, Kafka 4 & Java 25 baseline
Micronaut Kafka 6.0 requires the following minimum set of dependencies:
Java 25 or above
Kafka 4
Micronaut 5 or above
Major Enhancements
Micronaut Kafka 6.0 includes the following major enhancements:
@KafkaListener now supports an id member that is used to resolve consumer-specific configuration from kafka.consumers.* independently from the Kafka consumer group. When id is not set, Micronaut Kafka preserves the previous fallback behavior by using groupId.
@KafkaListener now supports consumerCreationStrategy = ConsumerCreationStrategy.PER_CLASS to create a single Kafka consumer for all topic methods in the listener class and route records to methods by the consumed topic. The default remains one consumer per @Topic method.
@KafkaScope provides a listener invocation scope for beans that should live for exactly one consumed record, or for one batch when the listener is configured for batch processing.
The default AdminClient bean can now be disabled with kafka.admin.enabled=false. When unrestricted Kafka health checks are enabled and admin support is disabled, the health indicator reports a clear DOWN result explaining that the AdminClient bean is unavailable.
Asynchronous and reactive @KafkaClient methods now use Micronaut’s blocking executor by default when no explicit executor is configured, avoiding Kafka producer work on the caller thread while preserving explicitly configured executors.
Kafka metric names now default to Micrometer style
Micronaut Kafka 5 changes the default naming for consumer and producer Micrometer metrics to use Micrometer compatible names. For example, kafka.consumer.bytes-consumed-total is now exported as kafka.consumer.fetch.manager.bytes.consumed.total.
This is a breaking change if you have dashboards, alerts, or scraping rules that depend on the previous names.
To preserve the previous Micronaut naming during migration, set micronaut.metrics.binders.kafka.metric-name-style=legacy.
3 Using the Micronaut CLI
To create a project with Kafka support using the Micronaut CLI, supply the kafka feature to the features flag.
$ mn create-app my-kafka-app --features kafka
This will create a project with the minimum necessary configuration for Kafka.
Kafka Messaging Application
The Micronaut CLI includes the ability to create Kafka-based messaging applications designed to implement message-driven microservices.
To create a Message-Driven Microservice with Micronaut + Kafka use the create-messaging-app command:
As you’d expect, you can start the application with ./gradlew run (for Gradle) or ./mvnw compile exec:exec (Maven). The application will (with the default config) attempt to connect to Kafka at http://localhost:9092, and will continue to run without starting up an HTTP server. All communication to/from the service will take place via Kafka producers and/or listeners.
Within the new project, you can now run the Kafka-specific code generation commands:
$ mn create-kafka-producer MessageProducer| Rendered template Producer.java to destination src/main/java/my/kafka/app/MessageProducer.java$ mn create-kafka-listener MessageListener| Rendered template Listener.java to destination src/main/java/my/kafka/app/MessageListener.java
To add support for Kafka to an existing project, you should first add the Micronaut Kafka configuration to your build configuration. For example in Gradle:
The kafka.bootstrap.servers value must be available before Kafka clients are initialized.
You may also add any Apache Kafka configuration options directly under the kafka node. These configurations will apply to consumers, producers and streams:
To create a Kafka Producer that sends messages you can simply define an interface that is annotated with @KafkaClient.
For example the following is a trivial @KafkaClient interface:
ProductClient.java
Note
You can omit the key, however this will result in a null key which means Kafka will not know how to partition the record.
At compile time Micronaut will produce an implementation of the above interface. You can retrieve an instance of ProductClient either by looking up the bean from the ApplicationContext or by injecting the bean with @Inject:
val client = beanContext.getBean(ProductClient::class.java)client.sendProduct("Nike", "Blue Trainers")
Note that since the sendProduct method returns void this means the method will send the ProducerRecord and block until the response is received. You can specify an executor and return either a CompletableFuture or Publisher to support non-blocking message delivery. When using CompletableFuture, a multi-threaded or per-task executor does not preserve invocation order between client method calls, so use a single-threaded executor if producer call ordering matters.
Creating a Kafka Consumer with @KafkaListener
To listen to Kafka messages you can use the @KafkaListener annotation to define a message listener.
The following example will listen for messages published by the ProductClient in the previous section:
ProductListener.java
Disabling Kafka
If for some reason, you need to disable the creation of kafka consumers, or producers, you can through configuration:
Disabling Kafka
kafka.enabled=false
kafka: enabled: false
[kafka]enabled = false
kafka { enabled = false}
{kafka = {enabled = false }}
{ "kafka": { "enabled": false }}
5 Serializing Messages with Avro and Protobuf
Micronaut can work with schema-based message formats such as Avro and Protocol Buffers in the same way it works with String, byte[], and JSON payloads. The @KafkaClient and @KafkaListener APIs do not change. The main difference is that you configure Kafka with serializer and deserializer implementations that understand your generated message types.
This section uses the Confluent Schema Registry serializers because they are a common choice for Avro and Protobuf deployments. If you use another serializer library, keep the Micronaut code the same and replace the Kafka serializer and deserializer classes with the equivalents from your chosen library.
What You Need
Before configuring Micronaut Kafka, make sure your application build also:
Generates Java, Groovy, or Kotlin classes from your Avro or Protobuf schemas
Adds the serializer library for the schema format you use
Provides a schema registry URL for development, testing, and production
Once those pieces are in place, use the generated message type directly in your @KafkaClient and @KafkaListener methods.
Avro Producer and Consumer Configuration
The following example configures a producer called books and a consumer group called book-group to publish and consume Avro messages through Schema Registry:
The specific.avro.reader setting tells the Avro deserializer to return your generated specific record type instead of a generic Avro record.
Protobuf Producer and Consumer Configuration
For Protocol Buffers, the serializer configuration is similar. The main additional setting is the generated message type that the deserializer should produce:
Replace example.DocumentEvent with the fully qualified name of the generated Protobuf message class that should be returned to your listener method.
Using the Generated Types in Clients and Listeners
After the serializers are configured, the Micronaut APIs stay the same:
In @KafkaClient, send the generated Avro or Protobuf type as the message body
In @KafkaListener, declare the same generated type in the listener method argument
Use producer ids and consumer group ids to scope serializer settings when different topics use different formats
This means you can mix String, JSON, Avro, and Protobuf clients in the same Micronaut application as long as each producer and consumer is configured with matching Kafka properties.
Testing Avro and Protobuf Clients
For broker integration tests, prefer the same local-development approach described in Running Kafka while testing and developing. Micronaut Test Resources can start Kafka automatically, and plain Testcontainers setups also work well.
If your tests do not need a real Schema Registry service, you can point the serializer to Confluent’s mock registry while still using a real Kafka broker:
That setup is often enough for tests that verify producer and listener wiring with generated Avro or Protobuf types. If you also want to validate registry compatibility rules or schema lifecycle behavior, run a real Schema Registry alongside Kafka in your integration tests.
When the same test suite needs multiple isolated registries, keep the shared Kafka settings under kafka.* and override the registry URL per producer id and consumer group id:
Configuring multiple mock schema registries in one test suite
Use the same producer ids that you assign to @KafkaClient and the same consumer group ids that you assign to @KafkaListener. The matching producer and consumer should point at the same registry URL, while unrelated clients can use a different mock scope or a different local registry endpoint entirely.
The same scoping pattern also works with other schema registry implementations. Replace the serializer and deserializer classes with the equivalents from your chosen library and point each producer or consumer configuration at the corresponding test endpoint.
6 Kafka Producers Using @KafkaClient
6.1 Defining @KafkaClient Methods
Specifying the Key and the Value
The Kafka key can be specified by providing a parameter annotated with @KafkaKey. If no such parameter is specified the record is sent with a null key.
The value to send is resolved by selecting the argument annotated with @MessageBody, otherwise the first argument with no specific binding annotation is used. For example:
The method above will use the parameter brand as the key and the parameter name as the value.
Including Message Headers
There are a number of ways you can include message headers. One way is to annotate an argument with the @MessageHeader annotation and include a value when calling the method:
The above example will send a header called X-Token with the value read from the setting my.application.token in application.yml (or the environnment variable MY_APPLICATION_TOKEN).
If the my.application.token is not set then an error will occur creating the client.
It is also possible to pass Collection<Header> or Headers object as method arguments as seen below.
In the above examples, all of the key/value pairs in headers will be added to the list of headers produced to the topic. Header and Headers are
part of the kafka-clients library:
Reactive and Non-Blocking Method Definitions
The @KafkaClient annotation supports the definition of reactive return types (such as Flowable or Reactor Flux) as well as Futures.
Note
The KafkaProducer used internally to implement @KafkaClient support is inherently blocking, even though some of its methods describe themselves as "asynchronous". Configuring an executor (as shown in the following examples) is required in order to guarantee that a returned reactive type or Future will not block the calling thread.
The following sections, which use Micronaut Reactor, cover advised configuration and possible method signatures and behaviour:
Configuring An Executor
As the send method of KafkaProducer can block the calling thread, it is recommended that you specify an executor to be used when returning either reactive types or CompletableFuture. This will ensure that the send logic is executed on a separate thread from that of the caller, and avoid undesirable conditions such as blocking of the Micronaut server’s event loop.
When using CompletableFuture, each @KafkaClient method invocation is submitted to the configured executor independently. If that executor can run multiple tasks concurrently, for example a fixed thread pool with more than one thread or a virtual-thread-per-task executor, the order in which your client methods are called is not guaranteed to match the order in which KafkaProducer.send is invoked. If producer call ordering matters, use a single-threaded executor or a synchronous method instead.
The executor to be used may be specified via configuration properties as in the following example:
The implementation will return a Mono that when subscribed to will subscribe to the passed Mono and send the emitted item as a ProducerRecord emitting the item again if successful or an error otherwise.
The implementation will return a Reactor Flux that when subscribed to will subscribe to the passed Flux and for each emitted item will send a ProducerRecord emitting the resulting Kafka RecordMetadata if successful or an error otherwise.
Available Annotations
There are a number of annotations available that allow you to specify how a method argument is treated.
The following table summarizes the annotations and their purpose, with an example:
Allows specifying the parameter that is used to compute a partition number independently from the Message Key.
@KafkaPartition String partitionKey
For example, you can use the @MessageHeader annotation to bind a parameter value to a header in the ProducerRecord.
6.2 Configuring @KafkaClient beans
@KafkaClient and Producer Properties
There are a number of ways to pass configuration properties to the KafkaProducer. You can set default producer properties using kafka.producers.default in application.yml:
Any property in the ProducerConfig class can be set, including any overrides over the global Micronaut Kafka configs. The above example will set the default number of times to retry sending a record as well as override kafka.bootstrap.servers.
Per @KafkaClient Producer Properties
To configure different properties for each client, you should set a @KafkaClient id using the annotation:
Using a Client ID
@KafkaClient("product-client")
@KafkaClient("product-client")
@KafkaClient('product-client')
This serves 2 purposes. Firstly it sets the value of the client.id setting used to build the Producer. Secondly, it allows you to apply per producer configuration in application.yml:
When serializing keys and values Micronaut will by default attempt to automatically pick a Serializer to use. This is done via the CompositeSerdeRegistry bean.
Tip
You can replace the default SerdeRegistry bean with your own implementation by defining a bean that uses @Replaces(CompositeSerdeRegistry.class). See the section on Bean Replacement.
All common java.lang types (String, Integer, primitives etc.) are supported and for POJOs by default a Jackson based JSON serializer is used.
You can, however, explicitly override the Serializer used by providing the appropriate configuration in application.yml:
By default if you define a method that takes a container type such as a List the list will be serialized using the specified value.serializer (the default will result in a JSON array).
For example the following two methods will both send serialized arrays:
In the above case instead of sending a serialized array the client implementation will iterate over each item in the list and send a ProducerRecord for each. The previous example is blocking, however you can return a reactive type if desired:
If you need maximum flexibility and don’t want to use the @KafkaClient support you can use the @KafkaClient annotation as qualifier for dependency injection of KafkaProducer instances.
Consider the following example:
Using a KafkaProducer directly
Note that there is no need to call the close() method to shut down the KafkaProducer, it is fully managed by Micronaut and will be shutdown when the application shuts down.
The previous example can be tested in JUnit with the following test:
Using a KafkaProducer directly
By using the KafkaProducer API directly you open up even more options if you require transactions (exactly-once delivery) or want control over when records are flushed etc.
6.5 Transactions
Transaction processing can be enabled by defining transactionalId on @KafkaClient, which will initialize the producer for transactional usage and wrap any send operation with a transaction demarcation.
@KafkaClient beans are by default singleton. When using multiple threads, you must either synchronize access to the individual instance or declare the bean as @Prototype. Additionally, you can use random properties to your advantage so that each instance of your producer gets a different transactional ID.
The quick start section presented a trivial example of what is possible with the @KafkaListener annotation.
Using the @KafkaListener annotation Micronaut will build a KafkaConsumer and start the poll loop by running the KafkaConsumer in a special consumer thread pool. You can configure the size of the thread pool based on the number of consumers in your application in application.yml as desired:
KafkaConsumer instances are single threaded, hence for each @KafkaListener method you define a new thread is created to execute the poll loop.
You may wish to scale the number of consumers you have listening on a particular topic. There are several ways you may achieve this. You could for example run multiple instances of your application each containing a single consumer in each JVM.
Alternatively, you can also scale via threads. By setting the number of threads a particular consumer bean will create:
Scaling with Threads
@KafkaListener(groupId = "myGroup", threads = 10)
@KafkaListener(groupId = "myGroup", threads = 10)
@KafkaListener(groupId='myGroup', threads = 10)
The above example will create 10 KafkaConsumer instances, each running in a unique thread and participating in the myGroup consumer group.
Note
@KafkaListener beans are by default singleton. When using multiple threads you must either synchronize access to local state or declare the bean as @Prototype.
You can also make your number of threads configurable by using threadsValue:
threads will be overridden by threadsValue if they are both set.
By default Micronaut will inspect the method signature of the method annotated with @Topic that will listen for ConsumerRecord instances and from the types infer an appropriate key and value Deserializer.
Properties under kafka.consumers.default serve as shared defaults applied to all ConsumerConfig-compatible consumers, while properties under kafka.consumers.<id> apply only to the consumer whose id matches (falling back to groupId when id is not set). The above example enables topic auto-creation for all consumers via the default key and sets a custom bootstrap server only for a listener declared with @KafkaListener(id = "product").
The @KafkaListener annotation examples up until now have been relatively trivial, but Micronaut offers a lot of flexibility when it comes to the types of method signatures you can define.
The following sections detail examples of supported use cases.
Specifying Topics
The @Topic annotation can be used at the method or the class level to specify which topics to be listened for.
Care needs to be taken when using @Topic at the class level because every public method of the class annotated with @KafkaListener will become a Kafka consumer, which may be undesirable.
Tip
You can make the topic name configurable using a placeholder: @Topic("${my.topic.name:myTopic}")
By default, Micronaut creates one Kafka consumer per @Topic-annotated listener method, even when multiple methods live in the same @KafkaListener class. To create a single consumer for the whole listener and route records to methods by topic, set consumerCreationStrategy = ConsumerCreationStrategy.PER_CLASS on @KafkaListener.
Handling Multiple Payload Types on One Topic
Kafka does not route records by @MessageBody type. If a topic intentionally carries multiple event types, prefer a single listener method that consumes a common supertype and branches on the concrete payload in application code.
Consuming a common supertype
@Topic("favorites-events")public void receive(@KafkaKey String customerId, FavoriteEvent event) { if (event instanceof FavoriteSaved) { // process the save event for this customer } else if (event instanceof FavoriteDeleted) { // process the delete event for this customer }}
@Topic("favorites-events")fun receive(@KafkaKey customerId: String, event: FavoriteEvent) { when (event) { is FavoriteSaved -> { // process the save event for this customer } is FavoriteDeleted -> { // process the delete event for this customer } }}
@Topic('favorites-events')void receive(@KafkaKey String customerId, FavoriteEvent event) { if (event instanceof FavoriteSaved) { // process the save event for this customer } else if (event instanceof FavoriteDeleted) { // process the delete event for this customer }}
Available Annotations
There are a number of annotations available that allow you to specify how a method argument is bound.
The following table summarizes the annotations and their purpose, with an example:
Allows binding a parameter to the partition the message was received from
@KafkaPartition Integer partition
For example, you can use the @MessageHeader annotation to bind a parameter value from a header contained within a ConsumerRecord.
Topics, Partitions and Offsets
If you want a reference to the topic, partition or offset it is a simple matter of defining a parameter for each.
The following table summarizes example parameters and how they related to the ConsumerRecord being processed:
Table 2. @KafkaListener Method Parameters
Parameter
Description
String topic
The name of the topic
long offset
The offset of the ConsumerRecord
int partition
The partition of the ConsumerRecord
long timestamp
The timestamp of the ConsumerRecord
As an example, following listener method will receive all of the above mentioned parameters:
Specifying Parameters for offset, topic etc.
Receiving a ConsumerRecord
If you prefer you can also receive the entire ConsumerRecord object being listened for. In this case you should specify appropriate generic types for the key and value of the ConsumerRecord so that Micronaut can pick the correct deserializer for each.
Consider the following example:
Specifying Parameters for offset, topic etc.
Receiving and returning Reactive Types
In addition to common Java types and POJOs you can also define listener methods that receive a Reactive type such as a Single or a Reactor Mono.
Note that in this case the method returns a Mono that indicates to Micronaut the poll loop should continue, and if enable.auto.commit is set to true (the default) the offsets will be committed, potentially before the doOnSuccess is called.
The idea here is that you are able to write consumers that don’t block, however care must be taken in the case where an error occurs in the doOnSuccess method otherwise the message could be lost. You could for example re-deliver the message in case of an error.
Alternatively, you can use the @Blocking annotation to tell Micronaut to subscribe to the returned reactive type in a blocking manner which will result in blocking the poll loop, preventing offsets from being committed automatically:
@KafkaListener(offsetReset = OffsetReset.EARLIEST)class ProductListener { @KafkaScope open class ProductMetadata { private val correlationId: String = UUID.randomUUID().toString() open fun correlationId(): String = correlationId } @Inject lateinit var productMetadata: ProductMetadata @Topic("products") fun receive(product: String) { println("Received $product with correlation ${productMetadata.correlationId()}") }}
The scoped bean is destroyed when the listener invocation completes, which makes it suitable for per-message state such as derived metadata, correlation data, or values that should stay stable for the duration of a single listener execution.
7.3 Intercepting Consumed Records
To inspect, filter, or wrap consumed records before Micronaut binds them to an @KafkaListener method, define a bean that implements ConsumerRecordInterceptor.
Interceptors run inside the listener invocation scope before argument binding for single-record listeners and before Micronaut builds the batch arguments for batch listeners. If multiple interceptor beans are present, Micronaut applies them in order.
Override the matches(..) method to limit an interceptor to specific listener beans or methods. The intercept(..) method receives an InterceptionContext that exposes the current ConsumerRecord together with listener metadata such as the client id, group id, topic, partition, and offset.
An interceptor can:
return the same ConsumerRecord
return a wrapped ConsumerRecord that preserves the original topic, partition, and offset
return null to skip listener invocation for that consumed record
Micronaut keeps using the consumed record coordinates for listener routing, retries, and offset management, so wrapped records must preserve the original topic, partition, and offset. When Micronaut manages commits, returning null still allows the configured offset strategy to advance for the consumed record. This makes the interceptor API suitable for header-based filtering without relying on Kafka consumer interceptors that can interfere with per-record commit handling.
If you need to instrument the underlying Kafka client itself, you can still use a BeanCreatedEventListener<Consumer<?, ?>>. Prefer ConsumerRecordInterceptor when the goal is to change what the listener sees before binding.
7.4 Configuring @KafkaListener beans
@KafkaListener and Consumer Groups
Kafka consumers created with @KafkaListener will by default run within a consumer group that is the value of micronaut.application.name unless you explicitly specify groupId or value, or provide an id to use as the group fallback. For example:
Specifying a Consumer Group
@KafkaListener("myGroup")
@KafkaListener("myGroup")
@KafkaListener('myGroup')
or
Specifying a Consumer Group alternative
@KafkaListener(groupId = "myGroup")
@KafkaListener(groupId = "myGroup")
@KafkaListener(groupId = 'myGroup')
The above examples will run the consumer within a consumer group called myGroup.
In this case, each record will be consumed by one consumer instance of the consumer group.
Note
Kafka delivers records per consumer group before Micronaut binds listener method arguments. If multiple @KafkaListener methods subscribe to the same topic with different group IDs, each group receives every record published to that topic.
Tip
You can make the consumer group configurable using a placeholder: @KafkaListener("${my.consumer.group:myGroup}")
The id member is used to resolve consumer-specific configuration from kafka.consumers.*.
If id is not specified, Micronaut falls back to groupId.
If groupId is not specified, Micronaut uses id as the Kafka consumer group unless group.id is already defined in configuration.
To allow the records to be consumed by all the consumer instances (each instance will be part of a unique consumer group), uniqueGroupId can be set to true:
Configuring Unique Group ID with Deletion on Shutdown
To achieve this behavior, the uniqueGroupId flag and uniqueGroupIdDeleteOnShutdown flag must be set to true. This ensures that when the consumer instance shuts down, its associated consumer group is deleted automatically.
There are a number of ways to pass configuration properties to the KafkaConsumer. You can set default consumer properties using kafka.consumers.default in application.yml:
The above example will set the default session.timeout.ms that Kafka uses to decide whether a consumer is alive or not and applies it to all created KafkaConsumer instances.
You can also provide configuration specific to a listener id. For example consider the following configuration:
The above configuration will pass properties to only the @KafkaListener beans with id = "myGroup", or listeners whose groupId is myGroup when id is not specified.
Finally, the @KafkaListener annotation itself provides a properties member that you can use to set consumer specific properties:
Configuring Consumer Properties with @KafkaListener
As mentioned previously when defining @KafkaListener methods, Micronaut will attempt to pick an appropriate deserializer for the method signature. This is done via the CompositeSerdeRegistry bean.
Tip
You can replace the default SerdeRegistry bean with your own implementation by defining a bean that uses @Replaces(CompositeSerdeRegistry.class). See the section on Bean Replacement.
All common java.lang types (String, Integer, primitives etc.) are supported and for POJOs by default a Jackson based JSON deserializer is used.
You can, however, explicitly override the Deserializer used by providing the appropriate configuration in application.yml:
There are a few options that can be enabled for only in the transactional processing:
Isolation
Use isolation member to define if you want to receive messages that haven’t been committed yet.
Custom offset strategy
There is a special offset strategy OffsetStrategy.SEND_TO_TRANSACTION that can only be used with an associated producer, only applicable when SendTo is used.
Only available when the transactional producer is enabled for @SendTo. Sends offsets to transaction using method sendOffsetsToTransaction of org.apache.kafka.clients.producer.Producer.
Depending on the your level of paranoia or durability requirements you can choose to tune how and when offsets are committed.
For SYNC_PER_RECORD and ASYNC_PER_RECORD, Micronaut tracks the next offset before invoking the listener and commits it only after the current record’s processing flow finishes.
If the listener completes normally, the current record is committed immediately.
If the listener throws and the error strategy resumes at the next record (RESUME_AT_NEXT_RECORD or LOG_AND_RESUME_AT_NEXT_RECORD), Micronaut handles the exception and then commits the failed record’s offset so consumption continues at the next offset.
If the listener throws and a retry strategy schedules another attempt, Micronaut seeks back to the failed offset and does not commit it while retries remain.
If the listener throws and a retry strategy exhausts its retries, Micronaut stops retrying that record and the current attempt commits past it unless stopOnExhaustedRetry = true, in which case Micronaut seeks back to the failed offset and pauses the affected partitions without committing the record.
If the listener throws and the deprecated NONE strategy is used, Micronaut stops the current poll() loop and does not perform an additional per-record commit for the failing record.
See the error strategy section for the meaning of each error strategy.
Manually Committing Offsets
If you set the OffsetStrategy to DISABLED it becomes your responsibility to commit offsets.
There are a couple of ways that can be achieved.
The simplest way is to define an argument of type Acknowledge and call the ack() method to commit offsets synchronously:
Committing offsets with ack()
Alternatively, you an supply a KafkaConsumer method argument and then call commitSync (or commitAsync) yourself when you are ready to commit offsets:
Committing offsets with the KafkaConsumer API
7.6 Assigning Kafka Offsets
7.6.1 Manually Assigning Offsets to a Consumer Bean
Sometimes you may wish to control exactly the position you wish to resume consuming messages from.
For example if you store offsets in a database you may wish to read the offsets from the database when the consumer starts and start reading from the position stored in the database.
To support this use case your consumer bean can implement the ConsumerSeekAware interface:
Manually seeking offsets with the ConsumerSeekAware API
Alternatively, when more fine-grained access to the Kafka consumer is required, your consumer bean can instead implement the ConsumerRebalanceListener and ConsumerAware interfaces:
Manually seeking offsets with the KafkaConsumer API
7.6.2 Manual Offsets with Multiple Topics
It is possible for a single @KafkaListener bean to represent multiple consumers. If you have more than one method annotated with @Topic then setKafkaConsumer will be called multiple times for each backing consumer.
It is recommended in the case of manually seeking offsets that you use a single listener bean per consumer, the alternative is to store an internal Set of all consumers associated with a particular listener and manually search for the correct listener in the onPartitionsAssigned using the partition assignment data.
Warning
Not doing so will lead to a ConcurrentModificationException error.
7.6.3 Manually Assigning Offsets from a Consumer Method
There may be some scenarios where you realize you need to seek to a different offset while consuming another one.
To support this use case, your consumer method can receive a KafkaSeekOperations instance as a parameter:
The seek operations will be performed by Micronaut automatically, when the consumer method completes successfully, possibly after committing offsets via OffsetStrategy.AUTO.
Tip
These operations determine the next offset retrieved by poll. Take into account that, even if the seek operation performs successfully, your consumer method may keep receiving records that were cached by the previous call. You can configure max.poll.records to control the maximum number of records returned by a single call to poll.
7.6.4 Creating Kafka Seek Operations
The interface KafkaSeekOperation.KafkaSeekOperation provides several static methods to create seek operations:
seek: Creates an absolute seek operation.
seekRelativeToBeginning: Creates a seek operation relative to the beginning.
seekToBeginning: Creates a seek to the beginning operation.
seekRelativeToEnd: Creates a seek operation relative to the end.
seekToEnd: Creates a seek to the end operation.
seekForward: Creates a forward seek operation.
seekBackward: Creates a backward seek operation.
seekToTimestamp: Creates a seek to the timestamp operation.
There may be cases where you prefer to receive all of the ConsumerRecord data from the ConsumerRecords holder object in one go.
To achieve this you can set the batch member of the @KafkaListener to true and specify a container type (typically List) to receive all of the data:
Receiving a Batch of Records
Note in the previous case offsets will automatically be committed for the whole batch by default when the method returns without error.
Manually Committing Offsets with Batch
As with one by one message processing, if you set the OffsetStrategy to DISABLED it becomes your responsibility to commit offsets.
If you want to commit the entire batch of offsets at once during the course of processing, then the simplest approach is to add an argument of type Acknowledgement and call the ack() method to commit the batch of offsets synchronously:
Committing a Batch of Offsets Manually with ack()
You can also take more control of committing offsets when doing batch processing by specifying a method that receives the offsets in addition to the batch:
Committing Offsets Manually with Batch
This example is fairly trivial in that it commits offsets after processing each record in a batch, but you can for example commit after processing every 10, or every 100 or whatever makes sense for your application.
Receiving a ConsumerRecords
When batching you can receive the entire ConsumerRecords object being listened for. In this case you should specify appropriate generic types for the key and value of the ConsumerRecords so that Micronaut can pick the correct deserializer for each.
This is useful when the need is to process or commit the records by partition, as the ConsumerRecords object already groups records by partition:
Commit only once for each partition
Reactive Batch Processing
Batch listeners also support defining reactive types (Reactor Flux or RxJava Flowable) as the method argument.
Remember that as with non batch processing, the reactive type will be subscribed to on a different thread and offsets will be committed automatically likely prior to the point when the reactive type is subscribed to.
This means that you should only use reactive processing if message durability is not a requirement and you may wish to implement message re-delivery upon failure.
7.8 Forwarding Messages with @SendTo
On any @KafkaListener method that returns a value, you can use the @SendTo annotation to forward the return value to the topic or topics specified by the @SendTo annotation.
The key of the original ConsumerRecord will be used as the key when forwarding the message.
Forwarding with @SendTo
You can also do the same using Reactive programming:
Forwarding Reactively with @SendTo
In the reactive case the poll loop will continue and will not wait for the record to be sent unless you specifically annotate the method with @Blocking.
To enable transactional sending of the messages you need to define producerTransactionalId in @KafkaListener.
Transactional consumer-producer
7.9 Handling Consumer Exceptions
Consumer error strategies
It’s possible to define a different error strategy for @KafkaListener using errorStrategy attribute:
Specifying an error strategy
@KafkaListener( value = "myGroup", errorStrategy = @ErrorStrategy( value = ErrorStrategyValue.RETRY_ON_ERROR, retryDelay = "50ms", retryCount = 3 ))
@KafkaListener( value = "myGroup", errorStrategy = ErrorStrategy( value = ErrorStrategyValue.RETRY_ON_ERROR, retryDelay = "50ms", retryCount = 3 ))
@KafkaListener( value = 'myGroup', errorStrategy = @ErrorStrategy( value = ErrorStrategyValue.RETRY_ON_ERROR, retryDelay = '50ms', retryCount = 3 ))
Setting the error strategy allows you to resume at the next offset or to seek the consumer (stop on error) to the failed offset so that it can retry if an error occurs.
You can choose one of the error strategies:
RETRY_ON_ERROR - This strategy will stop consuming subsequent records in the case of an error and by default will attempt to re-consume the current record once. Possible retry delay can be defined by retryDelay and retry count by retryCount.
RETRY_EXPONENTIALLY_ON_ERROR - This strategy will stop consuming subsequent records in the case of an error and by default will attempt to re-consume the current record once. The exponentially growing time breaks between consumption attempts is computed using the n * 2^(k - 1) formula where the initial delay n is retryDelay and the number of retries is retryCount.
RETRY_CONDITIONALLY_ON_ERROR - This strategy will stop consuming subsequent records in the case of an error and by default will attempt to re-consume the current record once. The retry behaviour can be overridden. Possible retry delay can be defined by retryDelay and retry count by retryCount.
RETRY_CONDITIONALLY_EXPONENTIALLY_ON_ERROR - This strategy will stop consuming subsequent records in the case of an error and by default will attempt to re-consume the current record once. The retry behaviour can be overridden. The exponentially growing time breaks between consumption attempts is computed using the n * 2^(k - 1) formula where the initial delay n is retryDelay and the number of retries is retryCount.
RESUME_AT_NEXT_RECORD - This strategy will ignore the current error and will resume at the next offset, in this case it’s recommended to have a custom exception handler that moves the failed message into an error queue.
LOG_AND_RESUME_AT_NEXT_RECORD - This strategy will publish the failed record to a dead letter topic, invoke the exception handler, and resume at the next offset. You must configure the target topic with dlq.
RETRY_TOPIC_ON_ERROR - This strategy will publish the failed record to derived retry topics and resume at the next offset on the original topic. Configure the retry topics with retryTopicSuffixes and retryTopicDelays. When retry topics are exhausted, Micronaut optionally publishes the record to dlq.
NONE - This error strategy will skip over all records from the current offset in the current poll when the consumer encounters an error. This option is deprecated and kept for consistent behaviour with previous versions of Micronaut Kafka that do not support error strategy.
Note
For batch listeners, LOG_AND_RESUME_AT_NEXT_RECORD is supported. The other error strategies apply only to non-batch message processing.
Note
RETRY_TOPIC_ON_ERROR requires direct topic names, not @Topic patterns. Retry topics must already exist in Kafka.
Note
When using retry error strategies in combination with reactive consumer methods, it is necessary to add the @Blocking annotation to the reactive consumer method.
For retry error strategies, set stopOnExhaustedRetry = true to pause the affected topic partitions after the last retryable failure instead of skipping past the failed record. The consumer seeks back to the failed offset before pausing, and you can resume it later through ConsumerRegistry.
When LOG_AND_RESUME_AT_NEXT_RECORD publishes a failed record to the dead letter topic, the original key, value, and headers are preserved. Micronaut also adds the following headers so the downstream consumer can inspect the failure context:
micronaut-kafka-exception-class
micronaut-kafka-exception-message
micronaut-kafka-original-topic
micronaut-kafka-original-partition
micronaut-kafka-original-offset
You can also make the number of retries configurable by using retryCountValue:
Dynamically Configuring Retries
@KafkaListener( value = "myGroup", errorStrategy = @ErrorStrategy( value = ErrorStrategyValue.RETRY_ON_ERROR, retryCountValue = "${my.retry.count}" ))
@KafkaListener( value = "myGroup", errorStrategy = ErrorStrategy( value = ErrorStrategyValue.RETRY_ON_ERROR, retryCountValue = "\${my.retry.count}" ))
@KafkaListener( value = 'myGroup', errorStrategy = @ErrorStrategy( value = ErrorStrategyValue.RETRY_ON_ERROR, retryCountValue = '${my.retry.count}' ))
Note
retryCountValue will be overridden by retryCount if they are both set.
Specify exceptions to retry
It’s possible to define only exceptions from which the retry will occur.
Specify exception to retry apply only for RETRY_ON_ERROR and RETRY_EXPONENTIALLY_ON_ERROR error strategies.
Conditional retries
It is possible to conditionally retry a message based on the exception thrown when the error strategy is RETRY_CONDITIONALLY_ON_ERROR or RETRY_CONDITIONALLY_EXPONENTIALLY_ON_ERROR.
Specifying conditional retry behaviour on the listener
@KafkaListener( value = "myGroup", errorStrategy = @ErrorStrategy( value = ErrorStrategyValue.RETRY_CONDITIONALLY_ON_ERROR ))public class ConditionalRetryListener implements ConditionalRetryBehaviourHandler { @Override public ConditionalRetryBehaviour conditionalRetryBehaviour(KafkaListenerException exception) { return shouldRetry(exception) ? ConditionalRetryBehaviour.RETRY : ConditionalRetryBehaviour.SKIP; } // ...
@KafkaListener( value = "myGroup", errorStrategy = ErrorStrategy( value = ErrorStrategyValue.RETRY_CONDITIONALLY_ON_ERROR ))class ConditionalRetryListener : ConditionalRetryBehaviourHandler { override fun conditionalRetryBehaviour(exception: KafkaListenerException): ConditionalRetryBehaviour { return if (shouldRetry(exception)) { ConditionalRetryBehaviour.RETRY } else { ConditionalRetryBehaviour.SKIP } } // ...
If you wish to apply the same conditional retry strategy for all of your @KafkaListener you can define a bean that implements ConditionalRetryBehaviourHandler and use Micronaut’s Bean Replacement feature to replace the default bean: @Replaces(DefaultConditionalRetryBehaviourHandler.class).
Note
Conditional retry behaviour only applies to RETRY_CONDITIONALLY_ON_ERROR and RETRY_CONDITIONALLY_EXPONENTIALLY_ON_ERROR error strategies.
Non-blocking retry topics
RETRY_TOPIC_ON_ERROR implements non-blocking retries by republishing the failed record to derived retry topics. Each retry topic suffix in retryTopicSuffixes must have a matching delay in retryTopicDelays.
@KafkaListener("product-retry-dlt-group")public class RetryTopicProductDltListener { @Topic("products-dlt") void receive(String product, MessageHeaders headers) { String originalTopic = headers.get("micronaut-kafka-original-topic", String.class).orElse("unknown"); System.out.printf("Routing %s from %s to the dead letter topic%n", product, originalTopic); }}
@KafkaListener("product-retry-dlt-group")class RetryTopicProductDltListener { @Topic("products-dlt") fun receive(product: String, headers: MessageHeaders) { val originalTopic = headers.get("micronaut-kafka-original-topic", String::class.java).orElse("unknown") println("Routing $product from $originalTopic to the dead letter topic") }}
@KafkaListener('product-retry-dlt-group')class RetryTopicProductDltListener { @Topic('products-dlt') void receive(String product, MessageHeaders headers) { String originalTopic = headers.get('micronaut-kafka-original-topic', String).orElse('unknown') System.out.printf('Routing %s from %s to the dead letter topic%n', product, originalTopic) }}
In this example, Micronaut derives products-retry-5s and products-retry-30s from the source topic products. The same listener processes the initial delivery and the retry topics, and micronaut-kafka-retry-attempt is populated only when the record is being retried.
When Micronaut publishes a failed record to a retry topic, it preserves the record key, value, and headers, updates the failure metadata headers, and adds:
micronaut-kafka-retry-attempt
micronaut-kafka-retry-due-timestamp
When a record arrives from a retry topic before its due timestamp, Micronaut pauses that retry partition until the configured delay expires and then processes the record.
When all retry topics are exhausted, Micronaut publishes the record to the configured dead letter topic, preserving the original topic metadata in the dead letter headers.
The following options are available to configure the default Kafka listener exception handler:
If you wish to replace this default exception handling with another implementation you can use the Micronaut’s Bean Replacement feature to define a bean that replaces it: @Replaces(DefaultKafkaListenerExceptionHandler.class).
You can run a Micronaut Kafka application with or without the presence of an HTTP server.
If you run your application without the http-server-netty dependency you will see output like the following on startup:
11:06:22.638 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 402ms. Server Running: 4 active message listeners.
No port is exposed, but the Kafka consumers are active and running. The process registers a shutdown hook such that the KafkaConsumer instances are closed correctly when the server is shutdown.
8.1 Kafka Health Checks
In addition to http-server-netty, if the management dependency is added, then Micronaut’s Health Endpoint can be used to expose the health status of the Kafka consumer application.
For example if Kafka is not available the /health endpoint will return:
{ "status": "DOWN", "details": { ... "kafka": { "status": "DOWN", "details": { "error": "java.util.concurrent.ExecutionException: org.apache.kafka.common.errors.TimeoutException: Timed out waiting for a node assignment." } } }}
Note
By default, the details visible above are only shown to authenticated users. See the Health Endpoint documentation for how to configure that setting.
The following options are available to configure the Kafka Health indicator:
If you do not wish to collect Kafka metrics, you can set micronaut.metrics.binders.kafka.enabled to false in application.yml.
In the case of Kafka Streams metrics, you can use micronaut.metrics.binders.kafka.streams.enabled instead.
For consumer and producer client metrics, Micronaut Kafka 5 uses Micrometer compatible metric names by default. For example, bytes-consumed-total is exported as kafka.consumer.fetch.manager.bytes.consumed.total.
This is a breaking change from earlier Micronaut Kafka versions, which exported legacy names such as kafka.consumer.bytes-consumed-total.
If you need to preserve the legacy Micronaut metric names during migration, configure the metric name style explicitly:
You can automatically add topics to the broker when your application starts. To do so, add a bean of type a NewTopic for each topic you want to create. NewTopic instances let you specify the name, the number of partitions, the replication factor, the replicas assignments and the configuration properties you want to associate with the new topic. Additionally, you can add a bean of type CreateTopicsOptions that will be used when the new topics are created.
Creating topics is not a transactional operation, so it may succeed for some topics while fail for others. This operation also executes asynchronously, so it may take several seconds until all the brokers become aware that the topics have been created.
If you ever need to check if the operation has completed, you can @Inject or retrieve the KafkaNewTopics bean from the application context and then retrieve the operation result that Kafka returned when the topics were created.
If you want to disable micronaut-kafka entirely, you can set kafka.enabled to false in application.yml.
This will prevent the instantiation of all kafka-related beans.
You must, however, provide your own replacement implementations of any @KafkaClient interfaces:
Creating Replacement KafkaClient Implementations
9 Kafka Streams
Tip
Using the CLI
If you are creating your project using the Micronaut CLI, supply the kafka-streams feature to include a simple Kafka Streams configuration in your project:
$ mn create-app my-app --features kafka-streams
Kafka Streams is a platform for building real time streaming applications.
When using Micronaut with Kafka Stream, your application gains all of the features from Micronaut (configuration management, AOP, DI, health checks etc.), simplifying the construction of Kafka Stream applications.
Since Micronaut’s DI and AOP is compile time, you can build low overhead stream applications with ease.
Defining Kafka Streams
To define Kafka Streams you should first add the kafka-streams configuration to your build.
You should then define an @Factory for your streams that defines beans that register topology components against the injected ConfiguredStreamBuilder. These beans can return a KStream, KTable, or GlobalKTable, depending on the topology you are building. For example to implement the Word Count example from the Kafka Streams documentation:
Note
If a stream topology is composed only of a KTable or GlobalKTable, make that bean eager (for example with @Context) so Micronaut initializes the topology before the corresponding KafkaStreams bean is resolved.
Kafka Streams Word Count
Note
With Kafka streams the key and value Serdes (serializer/deserializer) must be classes with a zero argument constructor. If you wish to use JSON (de)serialization you can subclass JsonObjectSerde to define your Serdes
You can use the @KafkaClient annotation to send a sentence to be processed by the above stream:
The above configuration example sets the processing.guarantee and auto.offset.reset setting of the default Stream. Most of the configuration properties pass directly through to the KafkaStreams instance being initialized.
In addition to those standard properties, you may want to customize how long you wait for Kafka Streams to shut down with close-timeout.
Micronaut Kafka integrates Kafka Streams with Micronaut’s graceful shutdown lifecycle. When the application is stopping, Micronaut waits for the configured streams to close, and close-timeout defines how long Micronaut waits for each stream before logging a timeout.
For example, this will make Micronaut Kafka wait for up to 10 seconds to shut down the default stream:
Configuring multiple Stream definitions on the same Micronaut Service.
You can define multiple Kafka Streams on the same Micronaut application, each with their own unique configuration.
To do this you should define the configuration with kafka.streams.[STREAM-NAME].
If you want to configure the primary, unqualified stream explicitly, use the default key and then define kafka.streams.[STREAM-NAME] for any additional named streams.
Each stream definition should also set its own unique application.id.
The above configuration sets the application.id to my-app-default-stream and num.stream.threads to 1 for the default stream. It also configures a named stream my-stream and a second named stream my-other-stream, each with their own application.id and num.stream.threads settings.
You can then inject an ConfiguredStreamBuilder specifically for the above configuration using jakarta.inject.Named:
The @Named qualifier must be applied to the injected ConfiguredStreamBuilder parameter so Micronaut can bind that topology to the matching kafka.streams.* configuration. If you have multiple topology beans and do not qualify the ConfiguredStreamBuilder, they share the default configuration such as client id and application id.
If an application includes the kafka-streams module but should not initialize any Kafka Streams beans in a given environment, set kafka.streams.enabled to false.
Configuring Kafka Streams for testing
When writing a test without starting the actual Kafka server, you can instruct Micronaut not to start Kafka Streams. To do this create a config file suffixed with an environment name, such as application-test.yml and set the kafka.streams.[STREAM-NAME].start-kafka-streams to false.
When using streams you can set a state store for your stream using a store builder and telling the stream to store its data. In the above example for the Kafka Streams Word Count, the output is materialized to a named store that can later be retrieved via the Interactive Query Service. Apache Kafka docs available here.
You can inject the InteractiveQueryService and use the method getQueryableStore(String storeName, QueryableStoreType<T> storeType) to get values from a state store.
An example service that wraps the InteractiveQueryService is included below. This is here to illustrate that when calling the getQueryableStore method you must provide the store name and preferably the type of key and value you are trying to retrieve.
import io.micronaut.configuration.kafka.streams.InteractiveQueryService;import io.micronaut.context.annotation.Requires;import jakarta.inject.Singleton;import org.apache.kafka.streams.state.QueryableStoreTypes;import org.apache.kafka.streams.state.ReadOnlyKeyValueStore;import java.util.Optional;/** * Example service that uses the InteractiveQueryService in a reusable way. This is only intended as an example. */@Singletonpublic class InteractiveQueryServiceExample { private final InteractiveQueryService interactiveQueryService; public InteractiveQueryServiceExample(InteractiveQueryService interactiveQueryService) { this.interactiveQueryService = interactiveQueryService; } /** * Method to get the word state store and word count from the store using the interactive query service. * * @param stateStore the name of the state store ie "foo-store" * @param word the key to get, in this case the word as the stream and ktable have been grouped by word * @return the Long count of the word in the store */ public Long getWordCount(String stateStore, String word) { Optional<ReadOnlyKeyValueStore<String, Long>> queryableStore = interactiveQueryService.getQueryableStore( stateStore, QueryableStoreTypes.keyValueStore()); return queryableStore.map(kvReadOnlyKeyValueStore -> kvReadOnlyKeyValueStore.get(word)).orElse(0L); } /** * Method to get byte array from a state store using the interactive query service. * * @param stateStore the name of the state store ie "bar-store" * @param blobName the key to get, in this case the name of the blob * @return the byte[] stored in the state store */ public byte[] getBytes(String stateStore, String blobName) { Optional<ReadOnlyKeyValueStore<String, byte[]>> queryableStore = interactiveQueryService.getQueryableStore( stateStore, QueryableStoreTypes.keyValueStore()); return queryableStore.map(stringReadOnlyKeyValueStore -> stringReadOnlyKeyValueStore.get(blobName)).orElse(null); } /** * Method to get value V by key K. * * @param stateStore the name of the state store ie "baz-store" * @param name the key to get * @return the value of type V stored in the state store */ public <K, V> V getGenericKeyValue(String stateStore, K name) { Optional<ReadOnlyKeyValueStore<K, V>> queryableStore = interactiveQueryService.getQueryableStore( stateStore, QueryableStoreTypes.<K, V>keyValueStore()); return queryableStore.map(kvReadOnlyKeyValueStore -> kvReadOnlyKeyValueStore.get(name)).orElse(null); }}
import io.micronaut.configuration.kafka.streams.InteractiveQueryServiceimport io.micronaut.context.annotation.Requiresimport jakarta.inject.Singletonimport org.apache.kafka.streams.state.QueryableStoreTypesimport org.apache.kafka.streams.state.ReadOnlyKeyValueStore/** * Example service that uses the InteractiveQueryService in a reusable way. This is only intended as an example. */@Singletonclass InteractiveQueryServiceExample(private val interactiveQueryService: InteractiveQueryService) { /** * Method to get the word state store and word count from the store using the interactive query service. * * @param stateStore the name of the state store ie "foo-store" * @param word the key to get, in this case the word as the stream and ktable have been grouped by word * @return the Long count of the word in the store */ fun getWordCount(stateStore: String, word: String): Long { val queryableStore = interactiveQueryService.getQueryableStore( stateStore, QueryableStoreTypes.keyValueStore<String, Long>()) return queryableStore.map { kvReadOnlyKeyValueStore: ReadOnlyKeyValueStore<String, Long> -> kvReadOnlyKeyValueStore[word] }.orElse(0L) } /** * Method to get byte array from a state store using the interactive query service. * * @param stateStore the name of the state store ie "bar-store" * @param blobName the key to get, in this case the name of the blob * @return the byte[] stored in the state store */ fun getBytes(stateStore: String, blobName: String): ByteArray? { val queryableStore = interactiveQueryService.getQueryableStore( stateStore, QueryableStoreTypes.keyValueStore<String, ByteArray>()) return queryableStore.map { stringReadOnlyKeyValueStore: ReadOnlyKeyValueStore<String, ByteArray> -> stringReadOnlyKeyValueStore[blobName] }.orElse(null) } /** * Method to get value V by key K. * * @param stateStore the name of the state store ie "baz-store" * @param name the key to get * @return the value of type V stored in the state store */ fun <K, V> getGenericKeyValue(stateStore: String, name: K): V { val queryableStore = interactiveQueryService.getQueryableStore( stateStore, QueryableStoreTypes.keyValueStore<K, V>()) return queryableStore.map { kvReadOnlyKeyValueStore: ReadOnlyKeyValueStore<K, V> -> kvReadOnlyKeyValueStore[name] }.orElse(null) }}
import io.micronaut.configuration.kafka.streams.InteractiveQueryServiceimport io.micronaut.context.annotation.Requires;import jakarta.inject.Singleton;import org.apache.kafka.streams.state.QueryableStoreTypes;import org.apache.kafka.streams.state.ReadOnlyKeyValueStore;/** * Example service that uses the InteractiveQueryService in a reusable way. This is only intended as an example. */@Singletonclass InteractiveQueryServiceExample { private final InteractiveQueryService interactiveQueryService; InteractiveQueryServiceExample(InteractiveQueryService interactiveQueryService) { this.interactiveQueryService = interactiveQueryService; } /** * Method to get the word state store and word count from the store using the interactive query service. * * @param stateStore the name of the state store ie "foo-store" * @param word the key to get, in this case the word as the stream and ktable have been grouped by word * @return the Long count of the word in the store */ Long getWordCount(String stateStore, String word) { Optional<ReadOnlyKeyValueStore<String, Long>> queryableStore = interactiveQueryService.getQueryableStore( stateStore, QueryableStoreTypes.keyValueStore()); return queryableStore.map(kvReadOnlyKeyValueStore -> kvReadOnlyKeyValueStore.get(word)).orElse(0L); } /** * Method to get byte array from a state store using the interactive query service. * * @param stateStore the name of the state store ie "bar-store" * @param blobName the key to get, in this case the name of the blob * @return the byte[] stored in the state store */ byte[] getBytes(String stateStore, String blobName) { Optional<ReadOnlyKeyValueStore<String, byte[]>> queryableStore = interactiveQueryService.getQueryableStore( stateStore, QueryableStoreTypes.keyValueStore()); return queryableStore.map(stringReadOnlyKeyValueStore -> stringReadOnlyKeyValueStore.get(blobName)).orElse(null); } /** * Method to get value V by key K. * * @param stateStore the name of the state store ie "baz-store" * @param name the key to get * @return the value of type V stored in the state store */ <K, V> V getGenericKeyValue(String stateStore, K name) { Optional<ReadOnlyKeyValueStore<K, V>> queryableStore = interactiveQueryService.getQueryableStore( stateStore, QueryableStoreTypes.<K, V>keyValueStore()); return queryableStore.map(kvReadOnlyKeyValueStore -> kvReadOnlyKeyValueStore.get(name)).orElse(null); }}
9.2 Kafka Stream Health Checks
In addition to http-server-netty, if the management dependency is added, then Micronaut’s Health Endpoint can be used to expose the health status of the Kafka streams application.
For example stream health at the /health endpoint will return:
By default, the details visible above are only shown to authenticated users. See the Health Endpoint documentation for how to configure that setting.
If you wish to disable the Kafka streams health check while still using the management dependency you can set the property kafka.health.streams.enabled to false in your application configuration.
Since version 2.8.0, Kafka allows you to handle uncaught exceptions that may be thrown from your streams. This handler must return the action that must be taken, depending on the thrown exception.
There are three possible responses: REPLACE_THREAD, SHUTDOWN_CLIENT, or SHUTDOWN_APPLICATION.
Tip
You can find more details about this mechanism here.
If you just want to take the same action every time, you can set the application property kafka.streams.[STREAM-NAME].uncaught-exception-handler to a valid action, such as REPLACE_THREAD.
If the handler returns SHUTDOWN_APPLICATION, Micronaut requests application shutdown and Kafka Streams participates in Micronaut’s graceful shutdown phase. This means the application lifecycle is stopped, health endpoints can transition accordingly, and Micronaut waits for the configured streams to close using each stream’s close-timeout.
To implement your own handler, you can listen to the application event BeforeKafkaStreamStart and configure the streams with your own business logic: