6.1 State Stores Explained
Module Overview
Goal: Make state stores understandable, not magical.
What You'll Learn:
- What state stores are and why they exist
- RocksDB vs in-memory stores
- Key-value vs window stores
- Read/write access patterns
- Store configuration and naming
- When and how to query stores
What's NOT Covered Here:
- Changelogs and replication (covered in Module 6.2)
- Restore and recovery (covered in Module 6.3)
- Interactive queries (covered in Module 11.1)
Key Insight: State stores are the physical storage layer that makes stateful operations possible.
1. What Are State Stores?
Definition
A state store is a local database where Kafka Streams maintains stateful operation results.
Stateful operation needs to remember:
├─ Aggregation results
├─ Join buffers
├─ Windowed data
└─ Custom state
State store provides:
├─ Fast local storage
├─ Key-value or windowed access
└─ Automatic management
Why State Stores Exist
Problem: Aggregations need memory
count(), reduce(), aggregate():
├─ Must remember previous results
├─ Update as new records arrive
└─ Make results queryable
Solution: State stores
├─ Persistent local storage
├─ Survives restarts (via changelogs)
└─ Fast read/write access
Where State Stores Live
Physical location:
/tmp/kafka-streams/application-id/0_0/
├─ rocksdb/
│ ├─ my-count-store/
│ │ ├─ 000001.sst
│ │ ├─ 000002.sst
│ │ └─ MANIFEST
│ └─ my-aggregate-store/
└─ .checkpoint
Each Streams instance:
└─ Has own state directory
└─ Stores state for assigned partitions
2. Types of State Stores
Key-Value Stores
Purpose: Store simple key-value pairs.
Use for:
├─ Regular aggregations (count, sum)
├─ KTable state
├─ Custom processors
Structure:
Key: "user123"
Value: 42
Window Stores
Purpose: Store values organized by time windows.
Use for:
├─ Windowed aggregations
├─ Time-based queries
Structure:
Key: "user123" + window(10:00-10:05)
Value: 15
Session Stores
Purpose: Store values organized by dynamic session windows.
Use for:
├─ Session window aggregations
├─ Activity-based windowing
Structure:
Key: "user123" + session(10:00-10:15)
Value: SessionData{...}
3. Storage Backends
RocksDB (Default)
What is RocksDB?
- Embedded key-value database (from Facebook)
- Stores data on disk
- Memory-efficient for large state
- Default for all state stores
Characteristics:
✅ Handles large state (GBs)
✅ Persists to disk
✅ Memory-efficient
✅ Production-ready
⚠️ Slower than in-memory
In-Memory Stores
Alternative: Store state in heap memory only.
Characteristics:
✅ Fastest access
✅ Simple (no disk I/O)
❌ Limited by heap size
❌ Lost on restart (without changelog)
❌ Not recommended for production
Use cases:
├─ Development/testing
├─ Small state (<100 MB)
└─ When speed > durability
Comparison
| Feature | RocksDB | In-Memory |
|---|---|---|
| Storage | Disk | Heap |
| Max size | TBs | Limited by heap |
| Speed | Fast | Fastest |
| Persistence | Yes | No (unless changelog) |
| Memory usage | Low | High |
| Production | ✅ Recommended | ⚠️ Not recommended |
4. Automatic State Store Creation
Implicit Creation
// State store created automatically
KTable<String, Long> counts = stream
.groupByKey()
.count(); // ← State store created behind the scenes
// Kafka Streams generates name:
// "KSTREAM-AGGREGATE-STATE-STORE-0000000001"
Explicit Naming
// Name your store explicitly
KTable<String, Long> counts = stream
.groupByKey()
.count(Materialized.as("user-counts"));
// State store named: "user-counts"
// Easier to:
// ├─ Query (Module 11.1)
// ├─ Monitor
// └─ Debug
5. Materialized: Configuring State Stores
What is Materialized?
Materialized configures how state is stored:
Materialized.<K, V, StateStore>as("store-name")
.withKeySerde(keySerde)
.withValueSerde(valueSerde)
.withCachingEnabled()
.withLoggingEnabled()
.withRetention(Duration.ofDays(7));
Basic Configuration
KTable<String, Long> counts = stream
.groupByKey()
.count(Materialized.<String, Long, KeyValueStore<Bytes, byte[]>>as("counts")
.withKeySerde(Serdes.String())
.withValueSerde(Serdes.Long())
);
Caching
// Enable caching (default: enabled)
Materialized.as("my-store")
.withCachingEnabled();
// Disable caching
Materialized.as("my-store")
.withCachingDisabled();
What caching does:
- Buffers updates in memory before writing to store
- Reduces I/O to RocksDB
- Reduces changelog writes
- Trade-off: slight delay in state updates
Changelog Configuration
// Enable changelog (default: enabled)
Materialized.as("my-store")
.withLoggingEnabled();
// Disable changelog (not recommended!)
Materialized.as("my-store")
.withLoggingDisabled();
Changelogs covered in Module 6.2.
Retention (for Windowed Stores)
// Set retention for windowed store
Materialized.<String, Long, WindowStore<Bytes, byte[]>>as("windowed-counts")
.withRetention(Duration.ofHours(24));
// Default retention:
// window size + grace period
Purpose: Delete old window data to prevent unbounded growth.
6. State Store Read/Write Patterns
Write Pattern (Automatic)
// Streams writes to store automatically
stream
.groupByKey()
.count() // Writes happen here
.toStream()
.to("output");
// You don't write directly
// Kafka Streams manages writes
Read Pattern (Interactive Queries)
// Read from store for queries
ReadOnlyKeyValueStore<String, Long> store =
streams.store(
StoreQueryParameters.fromNameAndType(
"user-counts",
QueryableStoreTypes.keyValueStore()
)
);
// Query store
Long count = store.get("user123");
System.out.println("User 123 count: " + count);
// Covered in detail in Module 11.1
7. State Store Lifecycle
Creation
1. Topology defined
└─ State stores declared (via aggregations)
2. Application starts
└─ Stores created on disk
3. Tasks assigned
└─ Each task gets its own store instances
Updates
For each record processed:
1. Read current state from store
2. Apply aggregation function
3. Write updated state to store
4. (Optional) Write to changelog topic
Deletion
State store deleted when:
├─ Application.id changes (new app)
├─ Manually delete state directory
├─ Window retention expires (windowed stores)
└─ Task revoked (during rebalance)
8. State Store Partitioning
State is Partitioned
Just like Kafka topics, state is partitioned:
Topic: "clicks" (3 partitions)
Instance 1:
├─ Task 0 → Partition 0
│ └─ State store for Partition 0
└─ Task 1 → Partition 1
└─ State store for Partition 1
Instance 2:
└─ Task 2 → Partition 2
└─ State store for Partition 2
Each task maintains state for its partitions only
Implications
STATE IS NOT GLOBAL:
❌ Can't query all keys from one instance
❌ Each instance sees only its partitions
EXCEPT GlobalKTable:
✅ Full replication
✅ All instances have all data
Interactive queries (Module 11.1) handle distributed state.
9. State Store Size Management
Factors Affecting Size
State store size depends on:
1. Number of unique keys
└─ More keys = more state
2. Value size
└─ Larger values = more state
3. Number of windows (windowed stores)
└─ More windows = more state
4. Retention period
└─ Longer retention = more state
Example Size Calculation
Windowed aggregation:
├─ 100,000 unique users
├─ 5-minute windows
├─ 24-hour retention (288 windows)
├─ Value size: 50 bytes
Size = 100,000 keys × 288 windows × 50 bytes
= 1.44 GB per partition
With 6 partitions = 8.64 GB total
Size Optimization
1. Use appropriate window sizes
└─ Smaller windows = more windows = more state
2. Set correct retention
└─ Don't keep data longer than needed
3. Use compact value types
└─ Minimize serialized size
4. Partition appropriately
└─ More partitions = state spread across more instances
5. Use RocksDB (not in-memory)
└─ Handles large state efficiently
10. Monitoring State Stores
Important Metrics
JMX Metrics:
# State store size (bytes)
kafka.streams:type=stream-state-metrics,
client-id=*,
task-id=*,
state-store-name=*,
name=size-bytes
# RocksDB metrics
kafka.streams:type=stream-state-metrics,
client-id=*,
task-id=*,
state-store-name=*,
rocksdb-state-id=*,
name=*
Checking State Directory
# List state stores
ls -lh /tmp/kafka-streams/my-app/0_0/rocksdb/
# Check size
du -sh /tmp/kafka-streams/my-app/0_0/rocksdb/*
# Output:
# 1.2G user-counts
# 500M product-aggregates
# 2.1G windowed-stats
11. Custom State Stores
When to Use Custom Stores
Most use cases: use built-in stores
├─ KeyValueStore
├─ WindowStore
└─ SessionStore
Custom stores for:
├─ Special data structures (lists, sets)
├─ Custom indexing
└─ Advanced use cases
Example: Using Processor API
// Define custom store
StoreBuilder<KeyValueStore<String, List<Event>>> storeBuilder =
Stores.keyValueStoreBuilder(
Stores.persistentKeyValueStore("my-custom-store"),
Serdes.String(),
listSerde
);
// Add to topology
builder.addStateStore(storeBuilder);
// Use in processor
builder.stream("input")
.process(() -> new MyProcessor("my-custom-store"), "my-custom-store");
// MyProcessor can read/write to store
class MyProcessor extends AbstractProcessor<String, Event> {
private KeyValueStore<String, List<Event>> store;
@Override
public void init(ProcessorContext context) {
store = (KeyValueStore) context.getStateStore("my-custom-store");
}
@Override
public void process(String key, Event value) {
List<Event> events = store.get(key);
if (events == null) events = new ArrayList<>();
events.add(value);
store.put(key, events);
}
}
Processor API is advanced topic beyond this course.
Accessing State Stores Programmatically
Using ProcessorContext:
When using the Processor API, you can access state stores via ProcessorContext.getStateStore(String name):
public class CustomProcessor extends AbstractProcessor<String, Value> {
private KeyValueStore<String, Long> stateStore;
@Override
public void init(ProcessorContext context) {
super.init(context);
// Access the state store by name
this.stateStore = (KeyValueStore<String, Long>)
context.getStateStore("my-state-store");
}
@Override
public void process(String key, Value value) {
// Read current state
Long currentCount = stateStore.get(key);
if (currentCount == null) currentCount = 0L;
// Update state
stateStore.put(key, currentCount + 1);
// Forward result
context().forward(key, currentCount + 1);
}
}
Adding Custom Store to Topology:
// Step 1: Create store builder
StoreBuilder<KeyValueStore<String, Long>> storeBuilder =
Stores.keyValueStoreBuilder(
Stores.persistentKeyValueStore("my-state-store"),
Serdes.String(),
Serdes.Long()
).withCachingEnabled(); // Optional: enable caching
// Step 2: Add to topology
Topology topology = new Topology();
topology.addStateStore(storeBuilder);
// Step 3: Connect processor to store
topology.addProcessor(
"my-processor",
() -> new CustomProcessor(),
"source-node"
).connectProcessorAndStateStores(
"my-processor",
"my-state-store" // Link processor to store
);
When to Use Processor API:
Use DSL (High-Level API):
├─ 95% of use cases
├─ count(), reduce(), aggregate()
├─ Cleaner, easier to understand
└─ Recommended for most applications
Use Processor API (Low-Level):
├─ Custom aggregation logic not supported by DSL
├─ Need fine-grained control over state access
├─ Complex punctuation/scheduling requirements
└─ Advanced optimization scenarios
SSD Performance Tip for State Restoration
Performance Tip: Using SSD (Solid State Drives) for the state directory significantly improves restoration speed after crashes or rebalances.
# Configure state directory on SSD mount
props.put(StreamsConfig.STATE_DIR_CONFIG, "/mnt/ssd/kafka-streams-state");
Impact:
HDD (Spinning Disk):
├─ Restoration: 50-100 MB/s
└─ 10 GB state = ~2-3 minutes
SSD:
├─ Restoration: 500-1000 MB/s
└─ 10 GB state = ~10-20 seconds
10x faster recovery with SSD!
Best Practice: Always use SSD for production Kafka Streams applications with significant state.
12. Common State Store Patterns
Pattern 1: Named Aggregation Store
// Name store for querying
KTable<String, Long> counts = stream
.groupByKey()
.count(Materialized.as("click-counts"));
// Later: query the store (Module 11.1)
Pattern 2: Windowed Store with Retention
stream
.groupByKey()
.windowedBy(TimeWindows.ofSizeAndGrace(
Duration.ofMinutes(5),
Duration.ofMinutes(1)))
.count(Materialized.<String, Long, WindowStore<Bytes, byte[]>>as("windowed-counts")
.withRetention(Duration.ofHours(1))
);
// Only keep 1 hour of windows
Pattern 3: In-Memory Store (Testing)
// Use in-memory for tests (fast, no disk I/O)
Materialized.<String, Long, KeyValueStore<Bytes, byte[]>>as(
Stores.inMemoryKeyValueStore("test-store"))
.withKeySerde(Serdes.String())
.withValueSerde(Serdes.Long());
13. State Store Best Practices
1. Always Name Your Stores
// Good
.count(Materialized.as("user-counts"))
// Bad (generated name like "KSTREAM-AGGREGATE-STATE-STORE-0000000003")
.count()
Why: Named stores are easier to monitor, query, and debug.
2. Use RocksDB for Production
// Default (RocksDB) - good
Materialized.as("my-store")
// In-memory - avoid in production
Materialized.as(Stores.inMemoryKeyValueStore("my-store"))
3. Set Appropriate Retention
// Good: explicit retention
Materialized.<String, Long, WindowStore<Bytes, byte[]>>as("windowed-counts")
.withRetention(Duration.ofHours(24))
// Bad: unbounded retention (state grows forever)
4. Monitor State Size
Set up alerts for:
├─ State store size exceeds threshold
├─ Disk usage on Streams instances
└─ Restore time (Module 6.3)
14. Troubleshooting State Stores
Problem: State Too Large
Symptoms:
├─ Disk full
├─ Slow processing
└─ Long restore times
Solutions:
1. Reduce retention
2. Use smaller windows
3. Add more partitions (spread state)
4. Compact values (smaller serialization)
Problem: State Corruption
Symptoms:
├─ Deserialization errors
├─ Unexpected null values
└─ Application crashes on startup
Solutions:
1. Delete state directory (will restore from changelog)
2. Check serde compatibility
3. Verify state store configuration
Problem: Cannot Query Store
Symptoms:
└─ store() throws exception
Causes:
├─ Store not materialized (no Materialized.as())
├─ Wrong store name
├─ Application not in RUNNING state
└─ Store not local (on different instance)
Solutions:
1. Name store explicitly
2. Wait for RUNNING state
3. Use store discovery (Module 11.1)
15. Mental Model Summary
State Store Hierarchy
KAFKA STREAMS APPLICATION
├─ Instance 1
│ ├─ Task 0 (Partition 0)
│ │ └─ State Store (RocksDB)
│ │ ├─ user-counts
│ │ └─ windowed-aggregates
│ └─ Task 1 (Partition 1)
│ └─ State Store (RocksDB)
│ ├─ user-counts
│ └─ windowed-aggregates
└─ Instance 2
└─ Task 2 (Partition 2)
└─ State Store (RocksDB)
├─ user-counts
└─ windowed-aggregates
Each task has its own store instances
State is partitioned like Kafka topics
State Store Lifecycle
1. Declaration (Materialized)
└─ Define store name, type, serdes
2. Creation (on app start)
└─ Physical directory created
└─ RocksDB initialized
3. Population (from changelog)
└─ State restored from backup
└─ Covered in Module 6.3
4. Operation (continuous)
└─ Read/write as records processed
5. Cleanup (on shutdown or retention)
└─ Old data deleted
└─ Resources released
16. Questions You Should Be Able to Answer
After this module, you should be able to answer:
✅ What is a state store?
✅ What's the difference between RocksDB and in-memory stores?
✅ What's the difference between key-value and window stores?
✅ How do I name a state store?
✅ What does Materialized do?
✅ Where are state stores physically located?
✅ How is state partitioned across instances?
✅ How do I monitor state store size?
17. What's Next?
Now that you understand state stores, the next modules will cover:
- Module 6.2: Changelogs & standby replicas — durability and availability
- Module 6.3: Restore & recovery — handling failures
- Module 11.1: Interactive queries — querying state stores
Summary
You learned:
- ✅ State stores are local databases where Kafka Streams maintains stateful operation results
- ✅ RocksDB (disk-based, recommended) vs in-memory (heap-based, testing only)
- ✅ Types: key-value, window, session stores
- ✅ Materialized configures store name, serdes, caching, logging, retention
- ✅ State is partitioned like Kafka topics (one store per task per partition)
- ✅ Named stores are easier to monitor and query
- ✅ State size depends on unique keys, windows, and retention
- ✅ Stores located in state directory (e.g.,
/tmp/kafka-streams/app-id/)
Next up: Module 6.2 — Changelogs & Standby Replicas — Learn how state is backed up and replicated for fault tolerance.