Skip to content

Kafka Streams — Stream Processing Complete Guide

DodaTech Updated 2026-06-20 10 min read

In this tutorial, you'll learn about Kafka Streams. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Kafka Streams is a lightweight Java library built on Apache Kafka for real-time Stream Processing without requiring a separate processing cluster.

What You'll Learn

In this tutorial, you'll learn Kafka Streams — the KStream and KTable abstractions, stateless and stateful transformations, Exactly-Once semantics, stream-table duality, and how to build a real-time processing topology with Java examples.

Why It Matters

Stream Processing is the backbone of modern Data Pipelines. Kafka Streams runs as a library inside your application — no separate cluster needed. Companies use it for real-time fraud detection, monitoring dashboards, and event-driven Microservices. Understanding it unlocks the ability to Process millions of events per second with minimal infrastructure.

Real-World Use

Uber processes ride events through Kafka Streams for real-time surge pricing. Coinbase tracks cryptocurrency transactions for fraud detection. Durga Antivirus Pro uses Kafka Streams to analyze threat intelligence feeds in real time — detecting attack patterns as they emerge.

Graph LR
  subgraph "Kafka Streams Topology"
    A[Kafka Topic: orders] --> B[KStream]
    B --> C[Filter: valid orders]
    C --> D[Map: enrich]
    D --> E[KTable: aggregate]
    E --> F[Sink: results topic]
  end
  G[State Store RocksDB] -.-> E

What is Kafka Streams?

Kafka Streams is a client library for building Stream Processing applications on top of Apache Kafka. It handles the hard parts — Partitioning, State management, fault tolerance — so you focus on the business logic.

Feature Kafka Streams Apache Flink Spark Streaming
Architecture Library (embedded) Cluster Cluster
Latency Sub-second Sub-second Seconds
State management RocksDB + Kafka topics RocksDB / Heap Checkpointing
Exactly-Once Yes (since 0.11) Yes Via transactions
Language Java / Scala Java / Python / Scala Scala / Python / Java

Stream-Table Duality

Think of a stream as a sequence of facts (events), and a table as a snapshot of the latest State. Every stream can be viewed as a changelog of a table, and every table can be reconstructed by replaying a stream. Kafka Streams gives you both: KStream for individual events and KTable for current State.

Setting Up Kafka Streams

Add the dependency to your pom.XML:

<!-- File: pom.XML -->
<dependency>
    <groupId>org.Apache.Kafka</groupId>
    <artifactId>Kafka-streams</artifactId>
    <version>3.7.0</version>
</dependency>

Now let's build our first topology.

Example 1: Word Count with KStream

This counts words from an input topic, writing results to an output topic. It's the "Hello World" of Stream Processing.

// File: WordCountApp.Java
// Requires: Kafka 3.7+, Java 11+
import org.Apache.Kafka.common.Serialization.Serdes;
import org.Apache.Kafka.streams.*;
import org.Apache.Kafka.streams.kstream.*;

import Java.util.Properties;

public class WordCountApp {
    public static void main(String[] args) {
        Properties props = new Properties();
        props.put(StreamsConfig.APPLICATION_ID_CONFIG, "word-count-app");
        props.put(StreamsConfig.Bootstrap_SERVERS_CONFIG, "localhost:9092");
        props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG,
                  Serdes.String().getClass().getName());
        props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG,
                  Serdes.String().getClass().getName());

        StreamsBuilder Builder = new StreamsBuilder();

        // Read input topic as a KStream
        KStream<String, String> textLines =
            Builder.stream("input-topic");

        // Split lines into words, count per word
        KTable<String, Long> wordCounts = textLines
            .flatMapValues(line -> Java.util.Arrays.asList(
                line.toLowerCase().split("\\W+")))
            .groupBy((key, word) -> word)
            .count();

        // Write results to output topic
        wordCounts.toStream().to("output-topic",
            Produced.with(Serdes.String(), Serdes.Long()));

        KafkaStreams streams = new KafkaStreams(Builder.build(), props);
        streams.start();

        // Graceful shutdown
        Runtime.getRuntime().addShutdownHook(
            new Thread(streams::close));
    }
}

Let's walk through this. The flatMapValues splits each line into individual words. groupBy repartitions by word so all occurrences of the same word reach the same partition. count aggregates into a KTable — the current count for each word.

Expected output (consuming output-topic):

hello    1
world    1
hello    2
flink    1
hello    3

Notice counts update incrementally — Kafka Streams outputs each change, not the final result. This is the power of Stream Processing: results update in real time as new data arrives.

Example 2: KTable Join — Enriching Events

Joining a stream of orders with a customer lookup table is a common real-world pattern. Kafka Streams makes this trivial.

// File: OrderEnrichmentApp.Java
// Enrich order stream with customer info
KStream<String, Order> orders =
    Builder.stream("orders",
        Consumed.with(Serdes.String(), orderSerde));

KTable<String, Customer> customers =
    Builder.table("customers",
        Consumed.with(Serdes.String(), customerSerde));

// Perform the join — enriches each order with customer name
KStream<String, EnrichedOrder> enriched = orders.join(
    customers,
    (order, customer) -> new EnrichedOrder(
        order.getOrderId(),
        customer.getName(),
        order.getAmount()),
    Joined.with(Serdes.String(), orderSerde, customerSerde)
);

enriched.to("enriched-orders",
    Produced.with(Serdes.String(), enrichedSerde));

The join works because both streams are keyed by the same value (customer ID). Kafka Streams ensures that for each order, it looks up the matching customer from the KTable — which is backed by a compacted topic.

Expected output (consuming enriched-orders):

{"orderId": "ORD-001", "customerName": "Alice", "amount": 250.00}
{"orderId": "ORD-002", "customerName": "Bob", "amount": 150.00}

Example 3: Stateful Transformation with Windowed Aggregation

Not all aggregations are unbounded. Often you need sliding windows — like "sales per hour" or "average temperature over 30 minutes."

// File: WindowedSalesApp.Java
// Hourly sales aggregated with hopping window
KStream<String, Double> sales =
    Builder.stream("sales",
        Consumed.with(Serdes.String(), Serdes.Double()));

// 1-hour tumbling window, advanced every 30 minutes
TimeWindowedKStream<String, Double> windowed = sales
    .groupByKey()
    .windowedBy(TimeWindows.ofSizeWithNoGrace(
        Java.time.Duration.ofHours(1))
        .advanceBy(Java.time.Duration.ofMinutes(30)));

KTable<Windowed<String>, Double> hourlyRevenue =
    windowed.reduce(Double::sum);

hourlyRevenue.toStream().to("hourly-revenue",
    Produced.with(
        WindowedSerdes.timeWindowedSerdeFrom(String.class, 3600000),
        Serdes.Double()));

This creates hopping windows — each hour of data is summed, with a new window starting every 30 minutes. The reduce function maintains running totals in the State store.

Expected output:

["2026-06-20T10:00", "store-1"]    4500.00
["2026-06-20T10:30", "store-1"]    7200.00
["2026-06-20T11:00", "store-1"]    3800.00

Exactly-Once Semantics

Kafka Streams supports Exactly-Once processing out of the box. Enable it with one config:

props.put(StreamsConfig.PROCESSING_GUARANTEE_CONFIG,
    StreamsConfig.EXACTLY_ONCE_V2);

This ensures that each input record is processed exactly once — even if the application crashes and restarts. It works through a combination of:

  1. Idempotent producers — duplicate writes are ignored
  2. Transactional writes — all output for an input batch commits atomically
  3. Consumer offsets management — offsets commit only when processing completes

Without this setting, a crash could produce duplicate results — a disaster for financial applications.

State Stores and Fault Tolerance

Kafka Streams stores State in embedded RocksDB instances, backed by changelog topics in Kafka. If a task crashes:

  1. The changelog topic contains every State change
  2. A new instance replays the changelog to rebuild State
  3. Processing resumes from the last committed offset

This is fully automatic — no manual recovery steps needed.

Common Mistakes

  1. Not configuring the State directory: State stores default to /tmp/kafka-streams. Explicitly set state.dir to a persistent path so State survives restarts — especially in containerized environments with Docker.

  2. Using groupByKey without repartition: groupByKey assumes data is already co-partitioned. If you change the key, data may not be correctly distributed. Use selectKey + through to repartition explicitly.

  3. Forgetting to handle tombstone records: In KTables, a record with a null value is a tombstone (deletion). If your downstream logic doesn't handle nulls, you'll get NullPointerException.

  4. Setting infinite retention with windowed aggregations: Unbounded State grows forever. Always set a retention period (until parameter) on windows or use withGrace to bound memory usage.

  5. Running multiple instances without unique application.id: Each Kafka Streams app instance in a group must share the same application.id. But if you run two unrelated apps, they need different IDs — otherwise they'll steal partitions from each other.

  6. Ignoring rebalance timeouts: Stateful applications take time to restore State during rebalances. Set session.timeout.ms and max.poll.interval.ms appropriately. Defaults assume fast restores — slow restores cause rebalancing loops.

  7. Mixing serde types: Kafka Streams is strict about serializer/deserializer types. A mismatch between Produced.with serde and actual data type throws a runtime error. Always test with a small dataset first.

Practice Questions

  1. What is the difference between KStream and KTable? A KStream represents a record stream (each record is an independent event). A KTable represents a changelog — each record is an update to the current State.

  2. How does Kafka Streams achieve fault tolerance? Through State stores backed by changelog topics in Kafka. On restart, State is rebuilt by replaying the changelog.

  3. What happens when you join a KStream with a KTable? For each KStream record, Kafka Streams looks up the current value in the KTable by key. This enables stream enrichment — adding reference data to event streams.

  4. What is a repartition operation and when is it needed? Repartitioning redistributes data across partitions by a new key. It's needed when groupBy changes the key, ensuring all records with the same key reach the same partition.

  5. How does windowing affect State management? Windows create State per window segment. Without retention boundaries, State grows unbounded. Always set window retention and grace periods.

Challenge

Build a Kafka Streams application that reads temperature sensor data, detects spikes (change > 10 degrees within 30 seconds), and emits an alert topic. Use a Sliding Window with a State store to track the previous reading per sensor.

Real-World Task

Deploy a Kafka Streams app on your local machine. Using the Kafka console consumer, observe how State is rebuilt after restarting the application. Use kafka-streams-application-reset.sh to reset the application's State and observe the behavior.

Mini Project: Real-Time Anomaly Detection

Build a system that monitors login attempts from a Kafka topic login-events. The Kafka Streams app counts failed attempts per user within a 5-minute window. If any user exceeds 5 failures, emit an alert to suspicious-users.

Security angle: This pattern is used in Durga Antivirus Pro to detect brute-force attacks across millions of endpoints in real time. The same logic powers Doda Browser's phishing detection — analyzing URL access patterns for anomalies.

// Pseudocode for anomaly detection
KStream<String, LoginEvent> logins = Builder.stream("login-events");

logins
    .filter((key, event) -> event.isFailed())
    .groupBy((key, event) -> event.getUserId())
    .windowedBy(TimeWindows.ofSizeWithNoGrace(
        Java.time.Duration.ofMinutes(5)))
    .count()
    .toStream()
    .filter((windowedUser, count) -> count > 5)
    .to("suspicious-users",
        Produced.with(
            WindowedSerdes.timeWindowedSerdeFrom(
                String.class, 300000),
            Serdes.Long()));

What is a Kafka Streams Topology?

A Kafka Streams topology is a directed acyclic Graph (DAG) of processors connected by streams, where each processor performs a transformation (filter, map, join, aggregate) on the data flowing through it.

How do you restart a Kafka Streams application?

Stop the application, run kafka-streams-application-reset.sh --application-id <id> --<a href="/frontend/bootstrap/bootstrap/">Bootstrap</a>-servers localhost:9092 to reset State, then start the application. Internal topics and State stores are rebuilt automatically.

FAQ

What's the difference between Kafka Streams and Kafka Consumer?

A Kafka Consumer reads messages one at a time. Kafka Streams builds on top of the consumer to provide State management, Partitioning, Exactly-Once semantics, and a declarative DSL (KStream/KTable). Use a Consumer for simple reads; use Kafka Streams for transformations, joins, and aggregations.

Can I use Kafka Streams without ZooKeeper?

Yes. Kafka Streams uses the Kafka broker's metadata APIs directly. With Kafka 3.x in KRaft mode (no ZooKeeper), Kafka Streams works without ZooKeeper entirely. Only the Kafka cluster itself needs ZooKeeper or KRaft.

Does Kafka Streams guarantee exactly-once delivery?

Yes, when processing.guarantee is set to exactly_once_v2. This ensures each record is processed exactly once, even during failures. It uses transactional producers and idempotent writes to prevent duplicates.

What's Next

Apache Flink — Next Lesson: True Stream Processing
Review: Apache Kafka Deep Dive
Related: Stream Processing Fundamentals
Related: Real-Time Analytics Guide

Learning Path

Graph LR
  A[Big Data Overview] --> B[Apache Kafka]
  B --> C[Kafka Streams]
  C --> D[Apache Flink / Spark]
  D --> E[Real-Time Analytics]
  style C fill:#4f46e5,stroke:#fff,stroke-width:2px,color:#fff

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

What's Next

Congratulations on completing this Kafka Streams tutorial. Here's where to Go from here:

  • Practice daily — Consistency beats long study sessions
  • Build a project — Apply what you learned with your own Kafka cluster
  • Explore related topics — Check out the Flink and Spark tutorials in this category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro