Kafka Streams — Cheat Sheet
Apache Kafka · 8 topics. Download the PDF or the Instagram carousel and share it.
Kafka Streams Basics
Kafka Streams is a Java client library for building stateful, fault-tolerant stream processing applications that read from and write to Kafka topics.
- ✓Kafka Streams is an embedded Java library — no separate processing cluster required
- ✓A topology is a DAG of source, processor, and sink nodes compiled from the Streams DSL
- ✓Stateful operations use RocksDB state stores backed by compacted changelog topics
- ✓On restart, state is restored from the changelog before processing resumes
- ✓Scaling: run multiple instances of the same application.id; Kafka redistributes partitions
- ✓Interactive Queries expose state store contents directly without an external database
@Configuration
@EnableKafkaStreams
class StreamConfig {
@Bean(name = KafkaStreamsDefaultConfiguration.DEFAULT_STREAMS_CONFIG_BEAN_NAME)
KafkaStreamsConfiguration streamsConfig() {
return new KafkaStreamsConfiguration(Map.of(
StreamsConfig.APPLICATION_ID_CONFIG, "order-processor",
StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "broker:9092",
StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass(),
StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass()
));
}
}
@Component
class OrderTopology {
@Autowired
void buildPipeline(StreamsBuilder builder) {
KStream<String, String> orders = builder.stream("orders-raw");
orders
.filter((key, value) -> value.contains(""status":"PAID""))
.mapValues(value -> enrichOrder(value))
.to("orders-enriched"); // sink topic
}
}KStream & KTable
KStream represents an unbounded event log; KTable represents a changelog-backed materialised view (latest value per key); GlobalKTable replicates fully to every instance.
- ✓KStream = append-only event log; every record is independent. KTable = changelog; only the latest value per key matters.
- ✓KStream-KTable joins look up the current table snapshot for each arriving stream event — useful for enrichment.
- ✓KStream-KStream joins require a time window because both sides are unbounded streams.
- ✓KTable state is backed by RocksDB locally and a changelog Kafka topic for fault tolerance and replication.
- ✓GlobalKTable replicates fully to every instance — safe for small lookup tables; avoid for large datasets.
- ✓Co-partitioning is required for KStream-KTable and KStream-KStream joins (same partition count, same partitioner).
StreamsBuilder builder = new StreamsBuilder();
// Source: read raw order events
KStream<String, OrderEvent> orders =
builder.stream("order-events",
Consumed.with(Serdes.String(), orderEventSerde));
// Stateless transforms
KStream<String, OrderEvent> validOrders = orders
.filter((key, order) -> order.getAmount().compareTo(BigDecimal.ZERO) > 0)
.mapValues(order -> {
order.setStatus("VALIDATED");
return order;
});
// Branch: route to different topics by value
Map<String, KStream<String, OrderEvent>> branches = validOrders.split()
.branch((key, order) -> "EXPRESS".equals(order.getShipping()),
Branched.withConsumer(s -> s.to("express-orders")))
.defaultBranch(Branched.withConsumer(s -> s.to("standard-orders")));
// Peek for side effects (logging, metrics) without mutating
validOrders.peek((key, order) ->
log.info("Processing order {} amount {}", key, order.getAmount()));
KafkaStreams streams = new KafkaStreams(builder.build(), config);
streams.start();Stream Processing Topology
A topology is a DAG of source, processor, and sink nodes; it is compiled from the high-level DSL (map, filter, join, aggregate) or the low-level Processor API.
- ✓A topology is a DAG: source nodes (from topics) → processor nodes (transform/filter/aggregate) → sink nodes (to topics)
- ✓KStream = unbounded sequence of records; KTable = last value per key (changelog semantics)
- ✓groupBy() + count()/aggregate() produces a KTable materialised in a named RocksDB state store
- ✓KStream-KTable join enriches each stream record with the latest value for the matching key in the table
- ✓Kafka Streams runs as a library in your application — no separate cluster or worker nodes needed
- ✓topology.describe() prints the full DAG structure — useful for debugging and understanding the processing graph
@Configuration
public class OrderStreamConfig {
@Bean
public KStream<String, OrderEvent> orderStream(StreamsBuilder builder) {
// Source: read from "order-events" topic
KStream<String, OrderEvent> stream = builder.stream(
"order-events",
Consumed.with(Serdes.String(), orderEventSerde())
);
// Filter: only PLACED orders
KStream<String, OrderEvent> placed = stream
.filter((key, event) -> event.getType() == OrderEventType.PLACED);
// Transform: enrich with metadata
KStream<String, EnrichedOrder> enriched = placed
.mapValues(event -> new EnrichedOrder(
event.getOrderId(),
event.getCustomerId(),
Instant.now()
));
// Branch: split by order value
Map<String, KStream<String, EnrichedOrder>> branches = enriched
.split(Named.as("branch-"))
.branch((k, v) -> v.getTotal().compareTo(new BigDecimal("1000")) > 0,
Branched.as("high-value"))
.defaultBranch(Branched.as("standard"));
// Sink: write to separate topics
branches.get("branch-high-value").to("high-value-orders");
branches.get("branch-standard").to("standard-orders");
return stream;
}
}Windowing in Kafka Streams
Tumbling windows (fixed non-overlapping), hopping windows (overlapping), session windows (activity-based gaps), and sliding windows each model different time-series aggregation needs.
- ✓Tumbling: non-overlapping fixed buckets. Hopping: overlapping windows sliding forward. Session: inactivity-gap based.
- ✓Grace period allows late-arriving events to update already-closed windows — critical for out-of-order data.
- ✓Kafka Streams uses event time (record timestamp) by default — tolerates out-of-order delivery correctly.
- ✓After the grace period expires, late events are silently dropped — monitor records-late-arrival metric.
- ✓Session windows merge when two sessions are closer than the inactivity gap — state store merges must be idempotent.
- ✓Use TopologyTestDriver with explicit timestamps to unit-test windowing logic without real Kafka.
StreamsBuilder builder = new StreamsBuilder();
KStream<String, OrderEvent> orders = builder.stream("order-events");
// TUMBLING window — count orders per product per 1-hour bucket
// Windows: [00:00–01:00], [01:00–02:00], [02:00–03:00] ...
KTable<Windowed<String>, Long> hourlyCounts = orders
.groupBy((key, order) -> order.getProductId())
.windowedBy(TimeWindows.ofSizeWithNoGrace(Duration.ofHours(1)))
// Grace period: accept late events for 5 minutes after window closes
// .windowedBy(TimeWindows.ofSizeAndGrace(Duration.ofHours(1), Duration.ofMinutes(5)))
.count(Materialized.as("hourly-product-counts"));
// Read windowed results
hourlyCounts.toStream()
.map((windowedKey, count) -> {
String productId = windowedKey.key();
long windowStart = windowedKey.window().start();
long windowEnd = windowedKey.window().end();
return KeyValue.pair(productId,
String.format("%s: %d orders in %d–%d", productId, count,
windowStart, windowEnd));
})
.to("hourly-product-counts-output");
// HOPPING window — 5-minute sum, updated every 1 minute (overlapping)
// Windows: [0:00–0:05], [0:01–0:06], [0:02–0:07] ...
orders.groupBy((k, v) -> v.getProductId())
.windowedBy(TimeWindows.ofSizeWithNoGrace(Duration.ofMinutes(5))
.advanceBy(Duration.ofMinutes(1)))
.count();State Stores & RocksDB
Stateful operations (aggregations, joins) persist state in RocksDB-backed changelog topics; state is restored automatically on restart, enabling fault-tolerant stateful processing.
- ✓State stores back stateful operations (aggregations, joins) with local RocksDB storage and a Kafka changelog topic
- ✓Changelog topics are compacted Kafka topics auto-created with name <app-id>-<store-name>-changelog
- ✓On restart, state is restored by replaying the changelog — this is what makes Kafka Streams fault-tolerant without external DB
- ✓In-memory stores (Stores.inMemoryKeyValueStore) are faster but lose state on restart and must replay full changelog
- ✓Standby replicas (num.standby.replicas) pre-build state on secondary instances, reducing restoration time on failover
- ✓Interactive Queries API (streams.store(…)) lets you query local state stores directly as a read model
// Word count with persistent state store (default)
StreamsBuilder builder = new StreamsBuilder();
KTable<String, Long> wordCounts = builder
.stream("sentences-input", Consumed.with(Serdes.String(), Serdes.String()))
.flatMapValues(value -> Arrays.asList(value.toLowerCase().split("\\W+")))
.groupBy((key, word) -> word)
.count(Materialized.<String, Long, KeyValueStore<Bytes, byte[]>>as("word-count-store")
.withKeySerde(Serdes.String())
.withValueSerde(Serdes.Long()));
// ↑ "word-count-store" is backed by RocksDB
// changelog topic: myapp-word-count-store-changelog
wordCounts.toStream().to("word-counts-output");
// application.properties for standby replicas
// spring.kafka.streams.properties.num.standby.replicas=1
// Standby instance keeps an up-to-date copy for fast failoverInteractive Queries
Interactive Queries expose state store contents over a REST API, turning the Kafka Streams instance into a queryable read model without an external DB.
- ✓State stores must be materialized with a name (Materialized.as("name")) to be queryable — anonymous stores are not accessible
- ✓streams.store(StoreQueryParameters.fromNameAndType(...)) returns a read-only view of the local state store
- ✓In multi-instance deployments, each instance owns only its assigned partitions — use queryMetadataForKey to find the owner
- ✓Configure application.server=host:port so instances can discover each other for distributed query proxying
- ✓WindowStore supports time-ranged fetch queries — retrieve aggregations for a key over a specific time range
- ✓Interactive Queries eliminate the need to sink aggregated state to an external database for simple read models
// 1. Materialize the state store with a name during topology build
StreamsBuilder builder = new StreamsBuilder();
KTable<String, Long> wordCounts = builder
.stream("text-input", Consumed.with(Serdes.String(), Serdes.String()))
.flatMapValues(v -> Arrays.asList(v.split("\\s+")))
.groupBy((k, word) -> word)
.count(Materialized.as("word-count-store")); // named → queryable
// 2. Query the store from a REST endpoint
@RestController
public class WordCountController {
private final KafkaStreams streams;
@GetMapping("/counts/{word}")
public Long getCount(@PathVariable String word) {
ReadOnlyKeyValueStore<String, Long> store = streams.store(
StoreQueryParameters.fromNameAndType(
"word-count-store",
QueryableStoreTypes.keyValueStore())
);
Long count = store.get(word);
return count != null ? count : 0L;
}
@GetMapping("/counts")
public Map<String, Long> getAllCounts() {
ReadOnlyKeyValueStore<String, Long> store = streams.store(
StoreQueryParameters.fromNameAndType(
"word-count-store", QueryableStoreTypes.keyValueStore()));
Map<String, Long> result = new HashMap<>();
try (KeyValueIterator<String, Long> it = store.all()) {
it.forEachRemaining(kv -> result.put(kv.key, kv.value));
}
return result;
}
}Kafka Streams State Stores
State stores are local RocksDB databases embedded in a Streams application enabling stateful operations like aggregations, joins, and windowed computations.
- ✓State stores are local RocksDB instances backed by Kafka changelog topics
- ✓Fault tolerance: state restored from changelog on task reassignment
- ✓Interactive queries allow external APIs to read state in real time
- ✓In-memory stores are faster but lose state on restart
- ✓num.standby.replicas pre-warms state on standby tasks for fast recovery
StreamsBuilder builder = new StreamsBuilder();
KStream<String, OrderEvent> orders = builder.stream("order-events");
KTable<String, CustomerStats> stats = orders
.groupByKey()
.aggregate(
CustomerStats::new,
(customerId, event, agg) -> { agg.add(event); return agg; },
Materialized.<String, CustomerStats, KeyValueStore<Bytes, byte[]>>
as("customer-stats-store")
.withValueSerde(new CustomerStatsSerde())
);
// Read state store from REST endpoint
@GetMapping("/stats/{customerId}")
public CustomerStats getStats(@PathVariable String customerId) {
ReadOnlyKeyValueStore<String, CustomerStats> store =
streams.store(StoreQueryParameters.fromNameAndType(
"customer-stats-store", QueryableStoreTypes.keyValueStore()));
return store.get(customerId);
}Kafka Streams Windowing
Windowing groups events into time-bounded buckets for aggregation. Kafka Streams supports tumbling, hopping, sliding, and session windows for real-time analytics.
- ✓Tumbling: fixed, non-overlapping — "count per minute"
- ✓Hopping: fixed, overlapping — "5-min rolling window, updated every minute"
- ✓Session: closes after inactivity gap — perfect for user sessions
- ✓Grace period allows late records; after grace, late records are dropped
- ✓Sliding windows update continuously with every event arrival
KStream<String, PageView> views = builder.stream("page-views");
// Tumbling — count per 1-minute bucket
KTable<Windowed<String>, Long> minuteCounts = views
.groupByKey()
.windowedBy(TimeWindows.ofSizeWithNoGrace(Duration.ofMinutes(1)))
.count(Materialized.as("views-per-minute"));
// Hopping — 5-min window advancing every 1 min
KTable<Windowed<String>, Long> hoppingCounts = views
.groupByKey()
.windowedBy(TimeWindows.ofSizeAndGrace(Duration.ofMinutes(5), Duration.ofSeconds(30))
.advanceBy(Duration.ofMinutes(1)))
.count();
// Session — user session with 30-min inactivity gap
KTable<Windowed<String>, Long> sessionCounts = views
.groupByKey()
.windowedBy(SessionWindows.ofInactivityGapAndGrace(
Duration.ofMinutes(30), Duration.ofMinutes(5)))
.count();