Kafka Streams — Stream Processing Complete Guide
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:
- Idempotent producers — duplicate writes are ignored
- Transactional writes — all output for an input batch commits atomically
- 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:
- The changelog topic contains every State change
- A new instance replays the changelog to rebuild State
- Processing resumes from the last committed offset
This is fully automatic — no manual recovery steps needed.
Common Mistakes
Not configuring the State directory: State stores default to
/tmp/kafka-streams. Explicitly setstate.dirto a persistent path so State survives restarts — especially in containerized environments with Docker.Using
groupByKeywithout repartition:groupByKeyassumes data is already co-partitioned. If you change the key, data may not be correctly distributed. UseselectKey+throughto repartition explicitly.Forgetting to handle tombstone records: In KTables, a record with a
nullvalue is a tombstone (deletion). If your downstream logic doesn't handle nulls, you'll getNullPointerException.Setting infinite retention with windowed aggregations: Unbounded State grows forever. Always set a retention period (
untilparameter) on windows or usewithGraceto bound memory usage.Running multiple instances without unique
application.id: Each Kafka Streams app instance in a group must share the sameapplication.id. But if you run two unrelated apps, they need different IDs — otherwise they'll steal partitions from each other.Ignoring rebalance timeouts: Stateful applications take time to restore State during rebalances. Set
session.timeout.msandmax.poll.interval.msappropriately. Defaults assume fast restores — slow restores cause rebalancing loops.Mixing serde types: Kafka Streams is strict about serializer/deserializer types. A mismatch between
Produced.withserde and actual data type throws a runtime error. Always test with a small dataset first.
Practice Questions
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.
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.
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.
What is a repartition operation and when is it needed? Repartitioning redistributes data across partitions by a new key. It's needed when
groupBychanges the key, ensuring all records with the same key reach the same partition.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 Next
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