{"id":15403633,"url":"https://github.com/rogervinas/spring-cloud-stream-kafka-step-by-step","last_synced_at":"2025-04-16T03:42:45.789Z","repository":{"id":44958678,"uuid":"349670276","full_name":"rogervinas/spring-cloud-stream-kafka-step-by-step","owner":"rogervinas","description":"🍀 Spring Cloud Stream Kafka - step by step","archived":false,"fork":false,"pushed_at":"2025-03-26T13:28:54.000Z","size":308,"stargazers_count":15,"open_issues_count":0,"forks_count":5,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-26T14:41:22.005Z","etag":null,"topics":["kafka","kotlin","spring-cloud-stream"],"latest_commit_sha":null,"homepage":"https://dev.to/rogervinas/spring-cloud-stream-step-by-step-11l0","language":"Kotlin","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rogervinas.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-03-20T08:36:22.000Z","updated_at":"2025-03-26T13:28:57.000Z","dependencies_parsed_at":"2024-02-14T09:47:29.641Z","dependency_job_id":"cf542361-e34b-42f3-a80b-38ae9010469a","html_url":"https://github.com/rogervinas/spring-cloud-stream-kafka-step-by-step","commit_stats":{"total_commits":101,"total_committers":3,"mean_commits":"33.666666666666664","dds":"0.40594059405940597","last_synced_commit":"e112618e582b6248b913b0970ed889a81839745b"},"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rogervinas%2Fspring-cloud-stream-kafka-step-by-step","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rogervinas%2Fspring-cloud-stream-kafka-step-by-step/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rogervinas%2Fspring-cloud-stream-kafka-step-by-step/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rogervinas%2Fspring-cloud-stream-kafka-step-by-step/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rogervinas","download_url":"https://codeload.github.com/rogervinas/spring-cloud-stream-kafka-step-by-step/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249191859,"owners_count":21227664,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["kafka","kotlin","spring-cloud-stream"],"created_at":"2024-10-01T16:09:30.516Z","updated_at":"2025-04-16T03:42:45.734Z","avatar_url":"https://github.com/rogervinas.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"![CI](https://github.com/rogervinas/spring-cloud-stream-kafka-step-by-step/actions/workflows/ci.yml/badge.svg)\n![Java](https://img.shields.io/badge/Java-21-blue?labelColor=black)\n![Kotlin](https://img.shields.io/badge/Kotlin-2.x-blue?labelColor=black)\n![SpringBoot](https://img.shields.io/badge/SpringBoot-3.x-blue?labelColor=black)\n![SpringCloud](https://img.shields.io/badge/SpringCloud-2024.x-blue?labelColor=black)\n\n# Spring Cloud Stream \u0026 Kafka binder step by step\n\n[Spring Cloud Stream](https://spring.io/projects/spring-cloud-stream) is the solution provided by **Spring** to build applications connected to shared messaging systems.\n\nIt offers an abstraction (the **binding**) that works the same whatever underneath implementation we use (the **binder**):\n* **Apache Kafka**\n* **Rabbit MQ**\n* **Kafka Streams**\n* **Amazon Kinesis**\n* ...\n\nLet's try to set up a simple example step by step and see how it works!\n\nThis demo has been created using this [spring initializr configuration](https://start.spring.io/#!type=gradle-project\u0026language=kotlin\u0026packaging=jar\u0026groupId=com.example\u0026artifactId=demo\u0026name=demo\u0026description=Demo%20project%20for%20Spring%20Boot\u0026packageName=com.example.demo\u0026dependencies=cloud-stream,web) adding Kafka binder dependency **spring-cloud-starter-stream-kafka**.\n\nStep by step:\n* [Producer with functional programming model](#producer-with-functional-programming-model)\n* [Consumer with functional programming model](#consumer-with-functional-programming-model)\n* [Extras](#extras)\n  * [Kafka Message Key](#kafka-message-key)\n  * [Retries](#retries)\n  * [Dead Letter Queue](#dead-letter-queue)\n* [Test this demo](#test-this-demo)\n* [Run this demo](#run-this-demo)\n* See also:\n  * :octocat: [Spring Cloud Stream \u0026 Kafka Confluent Avro Schema Registry](https://github.com/rogervinas/spring-cloud-stream-kafka-confluent-avro-schema-registry)\n  * :octocat: [Spring Cloud Stream \u0026 Kafka Streams Binder first steps](https://github.com/rogervinas/spring-cloud-stream-kafka-streams-first-steps)\n  * :octocat: [Spring Cloud Stream Multibinder](https://github.com/rogervinas/spring-cloud-stream-multibinder)\n  * :octocat: [Spring Cloud Stream \u0026 Kafka Streams Binder + Processor API](https://github.com/rogervinas/spring-cloud-stream-kafka-streams-processor)\n\nYou can browse older versions of this repo:\n* [Spring Boot 2.x with legacy annotations](https://github.com/rogervinas/spring-cloud-stream-kafka-step-by-step/tree/spring-boot-2.x-legacy-annotations) (deprecated since spring-cloud-stream:3.1)\n* [Spring Boot 2.x with functional programming model](https://github.com/rogervinas/spring-cloud-stream-kafka-step-by-step/tree/spring-boot-2.x)\n\n## Producer with functional programming model\n\nOur final goal is to produce messages to a Kafka topic.\n\nFrom the point of view of the application we want an interface `MyEventProducer` to produce events to a generic messaging system. These events will be of type `MyEvent`, just containing a `text` field to make it simpler:\n```kotlin\ndata class MyEvent(val text: String)\n\ninterface MyEventProducer { \n  fun produce(event: MyEvent)\n}\n```\n\nThen we follow these steps:\n\n### 1) We configure the binding `my-producer` in application.yml:\n```yaml\nspring:\n  cloud:\n    stream:\n      kafka:\n        binder:\n          brokers: \"localhost:9094\"\n      bindings:\n        my-producer-out-0:\n          destination: \"my.topic\"\n    function:\n      definition: \"my-producer\"\n``` \n* Everything under `spring.cloud.kafka.binder` is related to the Kafka binder implementation and we can use all these extra [Kafka binder properties](https://docs.spring.io/spring-cloud-stream-binder-kafka/docs/current/reference/html/spring-cloud-stream-binder-kafka.html#_kafka_binder_properties).\n* Everything under `spring.cloud.stream.bindings` is related to the Spring Cloud Stream binding abstraction and we can use all these extra [binding properties](https://docs.spring.io/spring-cloud-stream/docs/current/reference/html/spring-cloud-stream.html#binding-properties).\n* As stated in [functional binding names](https://docs.spring.io/spring-cloud-stream/docs/current/reference/html/spring-cloud-stream.html#_functional_binding_names): `my-producer` is the function name, `out` is for output bindings and `0` is the index we have to use if we have a single function.\n\n### 2) We create an implementation of `MyEventProducer` as a Kotlin lambda `() -\u003e Flux\u003cMyEventPayload\u003e`, to fulfill the interfaces that both our application and Spring Cloud Stream are expecting:\n```kotlin\nclass MyStreamEventProducer : () -\u003e Flux\u003cMyEventPayload\u003e, MyEventProducer {\n  private val sink = Sinks.many().unicast().onBackpressureBuffer\u003cMyEventPayload\u003e()\n\n  override fun produce(event: MyEvent) {\n    sink.emitNext(toPayload(event), FAIL_FAST)\n  }\n\n  override fun invoke() = sink.asFlux()\n\n  private fun toPayload(event: MyEvent) = MyEventPayload(event.text, event.text.length)\n}\n\ndata class MyEventPayload(\n  val string: String,\n  val number: Int\n)\n```\n* We use a DTO `MyEventPayload` to specify how do we want the payload to be serialized to JSON. In this case we don't need to but we could use [Jackson](https://github.com/FasterXML/jackson) annotations if we wanted to customize the JSON serialization.\n* We do a simple transformation between `MyEvent` and `MyEventPayload` just as an example.\n* Every time we emit a `MyEventPayload` through the `Flux`, Spring Cloud Stream will publish it to Kafka.\n\n### 3) Finally, we configure the beans needed to link `my-producer` function definition:\n```kotlin\n@Configuration\nclass MyConfiguration { \n  @Bean\n  fun myStreamEventProducer() = MyStreamEventProducer()\n  \n  @Bean(\"my-producer\")\n  fun myStreamEventProducerFunction(producer: MyStreamEventProducer): () -\u003e Flux\u003cMyEventPayload\u003e = producer\n}\n```\n* Both beans return the same instance ... why?\n  * We need an instance with type `MyStreamEventProducer` that will be injected wherever a `MyEventProducer` is needed.\n  * We need an instance with type `() -\u003e Flux\u003cMyEventPayload\u003e` that will be bound to `my-producer` function.\n    * As we are using **Kotlin** we need to define it as a lambda (required by **KotlinLambdaToFunctionAutoConfiguration**).\n    * If we were using **Java** we should define it as `Supplier\u003cFlux\u003cMyEventPayload\u003e\u003e`.\n\n### 4) For testing we start a Kafka container using [Testcontainers](https://www.testcontainers.org/):\n```kotlin\n@SpringBootTest(webEnvironment = NONE)\n@Testcontainers\n@ActiveProfiles(\"test\")\nclass MyApplicationIntegrationTest {\n  @Autowired // We inject MyEventProducer (it should be a MyStreamEventProducer)\n  @Qualifier(\"myStreamEventProducer\") // Avoid SpringBootTest issue: expected single matching bean but found 2  \n  lateinit var eventProducer: MyEventProducer\n    \n  @Test\n  fun `should produce event`() {\n    // We produce an event using MyEventProducer\n    val text = \"hello ${UUID.randomUUID()}\"\n    eventProducer.produce(MyEvent(text))\n\n    // We consume from Kafka using a helper\n    val records = consumerHelper.consumeAtLeast(1, FIVE_SECONDS)\n\n    // We verify the received json\n    assertThat(records).singleElement().satisfies { record -\u003e\n      JSONAssert.assertEquals(\n        record.value(),\n        \"{\\\"number\\\":${text.length},\\\"string\\\":\\\"$text\\\"}\",\n        true\n      )\n    }\n  }\n}\n```\n* Check the complete test in [MyApplicationIntegrationTest.kt](src/test/kotlin/com/rogervinas/stream/MyApplicationIntegrationTest.kt).\n\n## Consumer with functional programming model\n\nOur final goal is to consume messages from a Kafka topic.\n\nFrom the point of view of the application we want an interface `MyEventConsumer` to be called every time an event is consumed from a generic messaging system. These events will be of type `MyEvent` like in the producer example:\n```kotlin\ndata class MyEvent(val text: String)\n\ninterface MyEventConsumer {\n  fun consume(event: MyEvent)\n}\n```\n\nThen we follow these steps:\n\n### 1) We configure the binding `my-consumer` in application.yml declaring it as a function:\n```yaml\nspring:\n  cloud:\n    stream:\n      kafka:\n        binder:\n          brokers: \"localhost:9094\"\n      bindings:\n        my-consumer-in-0:\n          destination: \"my.topic\"\n          group: \"${spring.application.name}\"\n    function:\n      definition: \"my-consumer\"\n```\n* Remember that everything under `spring.cloud.kafka.binder` is related to the Kafka binder implementation and we can use all these extra [Kafka binder properties](https://docs.spring.io/spring-cloud-stream-binder-kafka/docs/current/reference/html/spring-cloud-stream-binder-kafka.html#_kafka_binder_properties) and everything under `spring.cloud.stream.bindings` is related to the Spring Cloud Stream binding abstraction and we can use all these extra [binding properties](https://docs.spring.io/spring-cloud-stream/docs/current/reference/html/spring-cloud-stream.html#binding-properties).\n* We configure a `group` because we want the application to consume from Kafka identifying itself as a consumer group so if there were to be more than one instance of the application every message will be delivered to only one of the instances.\n* As stated in [functional binding names](https://docs.spring.io/spring-cloud-stream/docs/current/reference/html/spring-cloud-stream.html#_functional_binding_names): `my-consumer` is the function name, `in` is for input bindings and `0` is the index we have to use if we have a single function.\n\n### 2) We create `MyStreamEventConsumer` to fulfill the interface required by Spring Cloud Stream:\n```kotlin\nclass MyStreamEventConsumer(private val consumer: MyEventConsumer) : (MyEventPayload) -\u003e Unit {\n  override fun invoke(payload: MyEventPayload) {\n    consumer.consume(fromPayload(payload))\n  }\n\n  private fun fromPayload(payload: MyEventPayload) = MyEvent(payload.string)\n}\n```\n* Every time a new message is received in the Kafka topic, its payload will be deserialized to a `MyEventPayload` and the `invoke` method will we called.\n* Then the only thing we have to do is to transform the `MyEventPayload` to a `MyEvent` and callback the generic `MyEventConsumer`.\n\n### 3) Finally, we configure the beans needed to link `my-consumer` function definition:\n```kotlin\n@Configuration\nclass MyConfiguration {\n  @Bean\n  fun myEventConsumer() = object : MyEventConsumer {\n    override fun consume(event: MyEvent) {\n      println(\"Received ${event.text}\")\n    }\n  }\n\n  @Bean(\"my-consumer\")\n  fun myStreamEventConsumerFunction(consumer: MyEventConsumer): (MyEventPayload) -\u003e Unit =\n    MyStreamEventConsumer(consumer)\n}\n```\n* We need an instance with type `(MyEventPayload) -\u003e Unit` that will be bound to `my-consumer` function.\n  * As we are using **Kotlin** we need to define it as a lambda (required by **KotlinLambdaToFunctionAutoConfiguration**).\n  * If we were using **Java** we should define it as `Consumer\u003cMyEventPayload\u003e`.\n* We create a simple implementation of `MyEventConsumer` that just prints the event.\n\n### 4) For testing we start a Kafka container using [Testcontainers](https://www.testcontainers.org/):\n```kotlin\n@SpringBootTest(webEnvironment = NONE)\n@Testcontainers\n@ActiveProfiles(\"test\")\nclass MyApplicationIntegrationTest {\n  @MockBean // We mock MyEventConsumer\n  lateinit var eventConsumer: MyEventConsumer\n\n  @Test\n  fun `should consume event`() {\n    val eventCaptor = argumentCaptor\u003cMyEvent\u003e()\n    doNothing().`when`(eventConsumer).consume(eventCaptor.capture())\n\n    // We send a Kafka message using a helper\n    val text = \"hello ${UUID.randomUUID()}\"\n    kafkaProducerHelper.send(TOPIC, \"{\\\"number\\\":${text.length},\\\"string\\\":\\\"$text\\\"}\")\n\n    // We wait at most 5 seconds to receive the expected MyEvent in MyEventConsumer mock\n    await().atMost(TEN_SECONDS).untilAsserted {\n      assertThat(eventCaptor.allValues.filter { it.text == text }).isEqualTo(ONE)\n    } \n  }\n}\n```\n* Check the complete test in [MyApplicationIntegrationTest.kt](src/test/kotlin/com/rogervinas/stream/MyApplicationIntegrationTest.kt).\n\n## Extras\n\n### Kafka Message Key\n\nKafka topics are partitioned to allow horizontal scalability.\n\nWhen a message is sent to a topic, Kafka chooses randomly the destination partition. If we specify a key for the message, Kafka will use this key to choose the destination partition, then all messages sharing the same key will always be sent to the same partition.\n\nThis is important on the consumer side, because **chronological order of messages is only guaranteed within the same partition**, so if we need to consume some messages in the order they were produced, we should use the same key for all of them (i.e. for messages of a *user*, we use the *user* id as the message key).\n\nTo specify the message key in `MyStreamEventProducer` we can produce `Message\u003cMyEventPayload\u003e` instead of `MyEventPayload` and inform the `KafkaHeaders.KEY` header:\n```kotlin\nclass MyStreamEventProducer : () -\u003e Flux\u003cMessage\u003cMyEventPayload\u003e\u003e, MyEventProducer {\n  // ...\n  override fun produce(event: MyEvent) {\n    val message = MessageBuilder\n      .withPayload(MyEventPayload(event.text, event.text.length))\n      .setHeader(KafkaHeaders.KEY, \"key-${event.text.length}\")\n      .build()\n    sink.emitNext(message, FAIL_FAST)\n  }\n  // ...\n}\n```\n\nAs we are setting a key of type `String` we should use a `StringSerializer` as `key.serializer`:\n```yaml\nspring:\n  cloud:\n    stream:\n      kafka:\n        binder:\n          brokers: \"localhost:9094\"\n          producer-properties:\n            key.serializer: \"org.apache.kafka.common.serialization.StringSerializer\"\n```\n\nAnd we can test it like this:\n```kotlin\n@Test\nfun `should produce event`() {\n  val text = \"hello ${UUID.randomUUID()}\"\n  eventProducer.produce(MyEvent(text))\n  \n  val records = kafkaConsumerHelper.consumeAtLeast(1, TEN_SECONDS)\n  \n  assertThat(records).singleElement().satisfies { record -\u003e\n    // check the message payload\n    JSONAssert.assertEquals(\n      record.value(),\n      \"{\\\"number\\\":${text.length},\\\"string\\\":\\\"$text\\\"}\",\n      true\n    )\n    // check the message key\n    assertThat(record.key())\n      .isEqualTo(\"key-${text.length}\")\n  }\n}\n```\n* Alternatively we can use `partitionKeyExpression` and other related [binding producer properties](https://docs.spring.io/spring-cloud-stream/docs/current/reference/html/spring-cloud-stream.html#_producer_properties) to achieve the same but at the binding abstraction level of Spring Cloud Stream.\n\n### Retries\n\nIf errors are thrown while consuming messages, we can tell Spring Cloud Stream what to do using the following [binding consumer properties](https://docs.spring.io/spring-cloud-stream/docs/current/reference/html/spring-cloud-stream.html#_consumer_properties):\n* **maxAttempts**: number of retries\n* **backOffInitialInterval**, **backOffMaxInterval**, **backOffMultiplier**: backoff parameters to increase delay between retries\n* **defaultRetryable**, **retryableExceptions**: which exceptions retry or not\n\nFor example we can use this configuration:\n```yaml\nspring:\n  cloud:\n    stream:\n      bindings:\n        my-consumer-in-0:\n          destination: \"my.topic\"\n          group: \"${spring.application.name}\"\n          consumer:\n            max-attempts: 5\n            back-off-initial-interval: 100\n            default-retryable: false\n            retryable-exceptions:\n              com.rogervinas.stream.domain.MyRetryableException: true            \n```\n\nAnd we can test it like this:\n```kotlin\n@Test\nfun `should retry consume event 5 times`() {\n  // we throw a MyRetryableException every time we receive a message\n  val eventCaptor = argumentCaptor\u003cMyEvent\u003e()\n  doThrow(MyRetryableException(\"retry later!\")).`when`(eventConsumer).consume(eventCaptor.capture())\n\n  // we send a Kafka message using a helper\n  val text = \"hello ${UUID.randomUUID()}\"\n  kafkaProducerHelper.send(TOPIC, \"{\\\"number\\\":${text.length},\\\"string\\\":\\\"$text\\\"}\")\n\n  // consumer has been called five times with the same message\n  await().atMost(TEN_SECONDS).untilAsserted {\n    assertThat(eventCaptor.allValues.filter { it.text == text }).isEqualTo(FIVE)\n  }\n}\n```\n\n### Dead Letter Queue\n\nAdditional to retries, DLQ is another mechanism we can use to deal with consumer errors.\n\nIn the case of Kafka it consists of sending to another topic all the messages that the consumer has rejected.\n\nWe can configure the DLQ using these [Kafka binder consumer properties](https://docs.spring.io/spring-cloud-stream-binder-kafka/docs/current/reference/html/spring-cloud-stream-binder-kafka.html#kafka-consumer-properties):\n* **enableDlq**: enable DLQ\n* **dlqName**:\n  * **not set**: defaults to `error.\u003cdestination\u003e.\u003cgroup\u003e`\n  * **set**: use a specific DLQ topic\n* **dlqPartitions**:\n  * **not set**: DLQ topic should have the same number of partitions as the original one\n  * **set to 0**: DLQ topic should have only 1 partition\n  * **set to N\u003e0**: we should provide a `DlqPartitionFunction` bean\n\nFor example we can use this configuration:\n```yaml\nspring:\n  cloud:\n    stream:\n      kafka:\n        binder:\n          brokers: \"localhost:9094\"\n        bindings:\n          my-consumer-in-0:\n            consumer:\n              enable-dlq: true\n              dlq-name: \"my.topic.errors\"\n              dlq-partitions: 1    \n      bindings:\n        my-consumer-in-0:\n          destination: \"my.topic\"\n          group: \"${spring.application.name}\"\n```\n\nAnd we can test it like this:\n\n#### Application errors:\n```kotlin\n@Test\nfun `should send to DLQ rejected messages`() {\n  // we throw a MyRetryableException every time we receive a message\n  doThrow(MyRetryableException(\"retry later!\")).`when`(eventConsumer).consume(any())\n\n  // we send a Kafka message using a helper\n  val text = \"hello ${UUID.randomUUID()}\"\n  kafkaProducerHelper.send(TOPIC, \"{\\\"number\\\":${text.length},\\\"string\\\":\\\"$text\\\"}\")\n\n  // we check the message has been sent to the DLQ\n  val errorRecords = kafkaDLQConsumerHelper.consumeAtLeast(1, TEN_SECONDS)\n  assertThat(errorRecords).singleElement().satisfies { record -\u003e\n    JSONAssert.assertEquals(\n      record.value(),\n      \"{\\\"number\\\":${text.length},\\\"string\\\":\\\"$text\\\"}\",\n      true\n    )\n  }\n}\n```\n\n#### Message deserialization errors:\n```kotlin\n@ParameterizedTest\n@ValueSource(strings = [\n  \"plain text\",\n  \"{\\\"unknownField\\\":\\\"not expected\\\"}\"\n])\nfun `send to DLQ undeserializable messages`(body: String) {\n  // we send a Kafka message with an invalid body using a helper\n  kafkaProducerHelper.send(TOPIC, body)\n\n  // we check the message has been sent to the DLQ\n  val errorRecords = kafkaDLQConsumerHelper.consumeAtLeast(1, TEN_SECONDS)\n  assertThat(errorRecords).singleElement().satisfies { record -\u003e\n    assertThat(record.value()).isEqualTo(body)\n  }\n}\n```\n\nThat's it! Happy coding! 💙\n\n## Test this demo\n\n```shell\n./gradlew test\n```\n\n## Run this demo\n\nRun with docker-compose:\n```shell\ndocker-compose up -d\n./gradlew bootRun\ndocker-compose down\n```\n\nThen you can use [kcat](https://github.com/edenhill/kcat) to produce/consume to/from Kafka:\n```shell\n# consume\nkcat -b localhost:9094 -C -t my.topic\nkcat -b localhost:9094 -C -t my.topic.errors\n\n# produce a valid message\necho '{\"string\":\"hello!\", \"number\":37}' | kcat -b localhost:9094 -P -t my.topic\n\n# produce an invalid message\necho 'hello!' | kcat -b localhost:9094 -P -t my.topic\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frogervinas%2Fspring-cloud-stream-kafka-step-by-step","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frogervinas%2Fspring-cloud-stream-kafka-step-by-step","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frogervinas%2Fspring-cloud-stream-kafka-step-by-step/lists"}