|
| 1 | += Kafka Streams |
| 2 | + |
| 3 | +This Spring Boot Application consists of a couple of examples using Kafka Streams with Kotlin and the Spring Framework. This example is rather "opinionated" - in that it does not delve into libraries like Spring Cloud Stream. Rather this example attempts to highlight the usage of dependency injection and configuring a Kafka Streams topology with Spring, while also providing syntactic examples from the Kotlin language. |
| 4 | + |
| 5 | +== Example 1: Yes, Word-Count |
| 6 | + |
| 7 | +This is a very familiar use case to anyone who has explored stream processing. Given an incoming event, the string value gets whitespace-tokenized into an array of strings. Then those strings are grouped such that case-insensitively identical strings are together, keyed by the word itself. |
| 8 | +We then materialize the counts of the words to a state store and emit those events with the counts to an output topic. |
| 9 | + |
| 10 | +```kotlin |
| 11 | + @Autowired |
| 12 | + fun buildPipeline(streamsBuilder: StreamsBuilder) { |
| 13 | + |
| 14 | + val messageStream = streamsBuilder |
| 15 | + .stream(INPUT_TOPIC, Consumed.with(STRING_SERDE, STRING_SERDE)) |
| 16 | + .peek {_, value -> logger.debug("*** raw value {}", value)} |
| 17 | + |
| 18 | + val wordCounts = messageStream |
| 19 | + .mapValues { v -> v.lowercase() } |
| 20 | + .peek {_, value -> logger.info("*** lowercase value = {}", value)} |
| 21 | + .flatMapValues { v -> v.split("\\W+".toRegex()) } |
| 22 | + .groupBy({ _, word -> word }, Grouped.with(Serdes.String(), Serdes.String())) |
| 23 | + .count(Materialized.`as`<String, Long>(Stores.persistentKeyValueStore(COUNTS_STORE)) |
| 24 | + .withKeySerde(STRING_SERDE) |
| 25 | + .withValueSerde(LONG_SERDE)) |
| 26 | + |
| 27 | + wordCounts.toStream().to(OUTPUT_TOPIC, Produced.with(STRING_SERDE, LONG_SERDE)) |
| 28 | + } |
| 29 | +``` |
| 30 | + |
| 31 | +== Example 2: Stream-Table Join |
| 32 | + |
| 33 | +This topology will filter the members, keeping only the ones of `PLATINUM` or `GOLD` level. Then it attempts a `join` on the |
| 34 | +checkins stream. The resulting matches are emitted to an output topic. |
| 35 | + |
| 36 | +```kotlin |
| 37 | + @Autowired |
| 38 | + fun buildPipeline(streamsBuilder: StreamsBuilder) { |
| 39 | + |
| 40 | + val checkins = streamsBuilder.stream(CHECKIN_TOPIC, Consumed.with(Serdes.String(), checkinSerde)) |
| 41 | + .peek { _, checkin -> logger.debug("checkin -> {}", checkin) } |
| 42 | + |
| 43 | + val members = streamsBuilder.table(MEMBER_TOPIC, Consumed.with(Serdes.String(), memberSerde)) |
| 44 | + .filter { _, m -> listOf(PLATINUM, GOLD).contains(m.membershipLevel) } |
| 45 | + |
| 46 | + val joined = checkins.join(members, { checkin, member -> |
| 47 | + logger.debug("matched member {} to checkin {}", member, checkin.txnId) |
| 48 | + EnrichedCheckin.newBuilder() |
| 49 | + .setMemberId(member.id) |
| 50 | + .setCheckinTxnId(checkin.txnId) |
| 51 | + .setTxnTimestamp(checkin.txnTimestamp) |
| 52 | + .setMembershipLevel(member.membershipLevel) |
| 53 | + .build() |
| 54 | + }, Joined.with(Serdes.String(), checkinSerde, memberSerde)) |
| 55 | + |
| 56 | + joined.to(ENRICHED_CHECKIN_TOPIC, Produced.with(Serdes.String(), enrichedCheckinSerde)) |
| 57 | + } |
| 58 | +``` |
| 59 | + |
| 60 | + |
| 61 | +== Run It |
| 62 | + |
| 63 | +=== Unit Tests |
| 64 | + |
| 65 | +To execute the unit tests, run the following gradle command (from the root of the project): |
| 66 | + |
| 67 | +```bash |
| 68 | +> ./gradlew :kafka-streams:test |
| 69 | +``` |
| 70 | + |
| 71 | +=== Confluent Cloud |
| 72 | + |
| 73 | +Update the Confluent Cloud assets using the terraform steps as outlined xref:../README.adoc#_confluent_cloud[here]. |
| 74 | + |
| 75 | +Now we can execute the application (with both topologies) using Gradle as follows (from the root of the project): |
| 76 | + |
| 77 | +```bash |
| 78 | +> ./gradlew :kafka-streams:bootRun |
| 79 | +``` |
| 80 | + |
| 81 | +Or you can use the execution features of you IDE. |
| 82 | + |
| 83 | +For the Word Count example, use the URL http://localhost:8080 as the basis for your using the REST endpoint for posting new words or querying the state store. |
0 commit comments