Documentation

kafka-streams

Kafka streams

Nov 21, 2025 • 5 min read

The Challenge of Real-Time Data

Simplifying Real-Time Analytics with Kafka Streams

In modern software development, waiting for nightly batch jobs to process data is often too slow. Whether you are building a fraud detection system, a live dashboard, or a recommendation engine, you need to process data as it arrives.

For many developers, moving from traditional “Request-Response” architectures to “Event-Driven” systems feels daunting. You might worry about managing complex clusters, handling state, or ensuring fault tolerance.

The Solution: Kafka Streams

Kafka Streams is a client library for building applications and microservices where the input and output data are stored in Kafka clusters. Unlike other stream processing frameworks (like Apache Flink or Spark), it is a library, not a cluster. This means you don’t need to manage a separate processing cluster; you just run your application like any other Java or Kotlin service.

Core Concepts

To understand Kafka Streams, you need to grasp two fundamental abstractions:

  1. KStream: Represents an unbounded stream of data. Think of it as a log where every new record is an insert.
  2. KTable: Represents a changelog stream. Think of it as a database table where each record is an upsert (update or insert) based on its key.

Getting Started

To use Kafka Streams, you simply add the dependency to your project and define your topology. Here is how you initialize a basic stream processing application:

StreamsBuilder builder = new StreamsBuilder();
KStream<String, String> source = builder.stream("input-topic");

// Processing logic here

source.to("output-topic");
KafkaStreams streams = new KafkaStreams(builder.build(), props);
streams.start();

Example: Real-time Word Count

Let’s look at the “Hello World” of streaming. Imagine we want to count the occurrences of words in a live stream of text.

With Kafka Streams, this complex stateful operation is reduced to just a few lines of code:

KTable<String, Long> wordCounts = textLines
    .flatMapValues(text -> Arrays.asList(text.toLowerCase().split("\\W+")))
    .groupBy((key, word) -> word)
    .count();

In the background, Kafka Streams handles the State Store for you. If your application crashes, it uses a “changelog topic” in Kafka to restore the counts and resume exactly where it left off.

<Input
        ref={renameInputRef}
        variant="inline"
        value={renameValue}
        onChange={(e) => onRenameValueChange(e.target.value)}
        onBlur={() => void onRenameSubmit()}
        onKeyDown={(e) => {
          if (e.key === "Enter") void onRenameSubmit();
          if (e.key === "Escape") onRenameCancel();
        }}
        onMouseDown={(e) => e.stopPropagation()}
        onClick={(e) => e.stopPropagation()}
        placeholder="Name"
        aria-label="Rename"
        className="flex-1 min-w-0"
      />

Why use it?

  • No separate cluster: It runs as part of your application.
  • Scalability: It leverages Kafka’s consumer groups to parallelize processing.
  • Exactly-once processing: It guarantees that each record is processed once and only once, even if there are failures.

Note:

Since Kafka Streams is a library, your application’s performance is tied to the resources of the machine it runs on. For heavy stateful operations, ensure your instances have enough local disk space for the RocksDB state stores.

History