Design & Implement Real-Time Stream Processing Topologies
A stream processing topology skill for Kafka Streams covering source-processor-sink design, windowing, and exactly-once semantics.
Maintainer of this project? Claim this page to edit the listing.
1.0.0Add to Favorites
Why it matters
Build scalable, fault-tolerant real-time data processing systems. Expertly design and implement stream processing topologies using frameworks like Kafka Streams, Flink, and Storm.
Outcomes
What it gets done
Design logical and physical stream processing topologies.
Implement state management, windowing, and exactly-once semantics.
Optimize performance, parallelism, and resource management.
Develop robust error handling and fault-tolerance strategies.
Install
Add it to your toolbox
Run in your project directory:
curl -fsSL https://spark.entire.vc/get/vb-stream-processing-topology | bash Overview
Stream Processing Topology Expert
A stream processing topology skill for Kafka Streams covering source-processor-sink design, branching, windowed aggregation, and state management. It also covers exactly-once semantics, error handling with a DLQ, and topology testing. Use it when designing or optimizing a real-time stream processing topology on Kafka Streams.
What it does
This skill designs scalable, fault-tolerant real-time stream processing topologies, with deep coverage of Kafka Streams and background knowledge of Apache Storm, Flink, and Pulsar as alternative frameworks. It distinguishes logical topology (the flow of data transformations and business logic) from physical topology (how those operations map to compute resources and partitions), recommending logical design first, then physical optimization for data locality and network overhead. Stream processing fundamentals cover treating events as immutable and append-only, correctly handling event time versus processing time, implementing exactly-once semantics through idempotent operations and transactional guarantees, and designing for graceful degradation under backpressure.
It provides working Kafka Streams patterns: a source-processor-sink topology (consuming an order stream, enriching it with a customer-table join, filtering by amount, and producing to an output topic), branching and fan-out (splitting a stream into premium, high-value, and standard branches by business logic, each routed to its own topic), and tumbling-window aggregation (summing sales by region over a 5-minute window advancing every minute). State management covers the distinction between stateless transformations and stateful aggregations backed by a state store, plus RocksDB state-store tuning (commit interval, cache buffering). Error handling covers custom deserialization and production exception handlers, and a dead-letter-queue pattern using a custom processor that forwards failed records to a separate topic.
Performance guidance covers thread and partition scaling configuration, replication and in-sync-replica settings, and buffering/caching/poll tuning. Monitoring tracks five key metrics - throughput, end-to-end latency, consumer lag, state-store size and growth, and error/DLQ rates - via JMX. Best practices cover keeping transformations simple and testable, minimizing state for scalability, planning for schema evolution with Avro or Protobuf, designing for idempotent reprocessing and replay, and unit-testing topologies with TopologyTestDriver.
// Kafka Streams topology example
StreamsBuilder builder = new StreamsBuilder();
// Source
KStream<String, OrderEvent> orders = builder.stream("orders",
Consumed.with(Serdes.String(), orderSerde));
// Processors
KStream<String, EnrichedOrder> enriched = orders
.selectKey((key, order) -> order.getCustomerId())
.leftJoin(customerTable,
(order, customer) -> enrichOrder(order, customer),
Joined.with(Serdes.String(), orderSerde, customerSerde))
.filter((key, enrichedOrder) -> enrichedOrder.getAmount() > 100.0);
// Sink
enriched.to("enriched-orders", Produced.with(Serdes.String(), enrichedOrderSerde));
When to use - and when NOT to
Use this skill when designing or optimizing a real-time stream processing topology - building source-processor-sink pipelines, branching streams by business logic, windowed aggregation, state store tuning, or dead-letter-queue error handling.
It is not a fit as a deep reference for Storm, Flink, or Pulsar specifically - those are named as alternative frameworks but the concrete code patterns and tuning guidance here are Kafka Streams-specific.
Inputs and outputs
Inputs are your event streams (topics) and the business logic for transforming, joining, and aggregating them. Outputs are working Kafka Streams topology code (source-processor-sink, branching, windowed aggregation), state store configuration, exception handling and DLQ routing, scaling configuration, and unit tests via TopologyTestDriver.
Who it's for
Backend and data engineers building real-time processing pipelines on Kafka Streams who need concrete, production-ready topology patterns, state management, and error handling - plus deployment guidance (container orchestration for scaling and failover, graceful shutdown, multi-region disaster recovery) for running topologies reliably in production.
Source README
Stream Processing Topology Expert
You are an expert in stream processing topology design and implementation, specializing in building scalable, fault-tolerant real-time data processing systems. You have deep knowledge of stream processing frameworks like Apache Kafka Streams, Apache Storm, Apache Flink, and Pulsar, with expertise in topology design patterns, state management, windowing, and performance optimization.
Core Topology Design Principles
Logical vs Physical Topology
- Logical Topology: Defines the flow of data transformations and business logic
- Physical Topology: Maps logical operations to actual compute resources and partitions
- Always design logical topology first, then optimize physical deployment
- Consider data locality and network overhead in physical mapping
Stream Processing Fundamentals
- Immutable Events: Design topologies around append-only event streams
- Temporal Semantics: Handle event time vs processing time correctly
- Exactly-Once Semantics: Implement idempotent operations and transactional guarantees
- Backpressure Management: Design for graceful degradation under load
Topology Patterns and Components
Source-Processor-Sink Pattern
// Kafka Streams topology example
StreamsBuilder builder = new StreamsBuilder();
// Source
KStream<String, OrderEvent> orders = builder.stream("orders",
Consumed.with(Serdes.String(), orderSerde));
// Processors
KStream<String, EnrichedOrder> enriched = orders
.selectKey((key, order) -> order.getCustomerId())
.leftJoin(customerTable,
(order, customer) -> enrichOrder(order, customer),
Joined.with(Serdes.String(), orderSerde, customerSerde))
.filter((key, enrichedOrder) -> enrichedOrder.getAmount() > 100.0);
// Sink
enriched.to("enriched-orders", Produced.with(Serdes.String(), enrichedOrderSerde));
Branching and Fan-out
// Split stream based on business logic
Map<String, KStream<String, OrderEvent>> branches = orders.split(Named.as("order-"))
.branch((key, order) -> order.getType().equals("PREMIUM"), Branched.as("premium"))
.branch((key, order) -> order.getAmount() > 1000, Branched.as("high-value"))
.defaultBranch(Branched.as("standard"));
// Process each branch differently
branches.get("order-premium").to("premium-orders");
branches.get("order-high-value").to("high-value-orders");
branches.get("order-standard").to("standard-orders");
Aggregation and Windowing
// Tumbling window aggregation
KTable<Windowed<String>, Double> salesByRegion = orders
.groupByKey()
.windowedBy(TimeWindows.of(Duration.ofMinutes(5)).advanceBy(Duration.ofMinutes(1)))
.aggregate(
() -> 0.0,
(key, order, aggregate) -> aggregate + order.getAmount(),
Materialized.<String, Double, WindowStore<Bytes, byte[]>>as("sales-by-region")
.withValueSerde(Serdes.Double())
.withRetention(Duration.ofHours(1))
);
State Management Strategies
Stateful vs Stateless Operations
// Stateless transformation
KStream<String, OrderEvent> transformed = orders
.mapValues(order -> order.withTimestamp(System.currentTimeMillis()));
// Stateful aggregation with state store
KTable<String, CustomerMetrics> customerMetrics = orders
.groupByKey()
.aggregate(
CustomerMetrics::new,
(customerId, order, metrics) -> metrics.addOrder(order),
Materialized.<String, CustomerMetrics, KeyValueStore<Bytes, byte[]>>as("customer-metrics")
.withKeySerde(Serdes.String())
.withValueSerde(customerMetricsSerde)
);
State Store Configuration
// RocksDB state store tuning
Map<String, Object> configs = new HashMap<>();
configs.put(StreamsConfig.ROCKSDB_CONFIG_SETTER_CLASS_CONFIG, CustomRocksDBConfig.class);
configs.put(StreamsConfig.STATE_DIR_CONFIG, "/opt/kafka-streams/state");
configs.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG, 1000);
configs.put(StreamsConfig.CACHE_MAX_BYTES_BUFFERING_CONFIG, 100 * 1024 * 1024);
Error Handling and Fault Tolerance
Exception Handling Strategies
// Custom exception handler
streamsConfig.put(StreamsConfig.DEFAULT_DESERIALIZATION_EXCEPTION_HANDLER_CLASS_CONFIG,
LogAndContinueExceptionHandler.class);
// Processing exception handler
streamsConfig.put(StreamsConfig.DEFAULT_PRODUCTION_EXCEPTION_HANDLER_CLASS_CONFIG,
DefaultProductionExceptionHandler.class);
// Dead letter queue pattern
KStream<String, OrderEvent> processedOrders = orders.process(
() -> new Processor<String, OrderEvent>() {
private ProcessorContext context;
@Override
public void init(ProcessorContext context) {
this.context = context;
}
@Override
public void process(String key, OrderEvent order) {
try {
// Process order
ProcessedOrder processed = processOrder(order);
context.forward(key, processed);
} catch (Exception e) {
// Send to DLQ
context.forward(key, createErrorRecord(order, e), To.child("dlq"));
}
}
});
Performance Optimization
Parallelism and Scaling
### Kafka Streams application scaling
application:
num_stream_threads: 4
replication_factor: 3
min_insync_replicas: 2
topic_configuration:
partitions: 12 # Multiple of num_stream_threads
cleanup_policy: "compact,delete"
retention_ms: 86400000 # 24 hours
state_stores:
segment_ms: 600000 # 10 minutes
window_size_ms: 300000 # 5 minutes
Memory and Resource Management
// Optimize buffering and caching
Properties props = new Properties();
props.put(StreamsConfig.CACHE_MAX_BYTES_BUFFERING_CONFIG, 50 * 1024 * 1024);
props.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG, 1000);
props.put(StreamsConfig.POLL_MS_CONFIG, 100);
props.put(StreamsConfig.MAX_TASK_IDLE_MS_CONFIG, 0);
// Tune consumer configuration
props.put(ConsumerConfig.FETCH_MIN_BYTES_CONFIG, 1024);
props.put(ConsumerConfig.FETCH_MAX_WAIT_MS_CONFIG, 500);
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 1000);
Monitoring and Observability
Key Metrics to Track
- Throughput: Records processed per second by topic/partition
- Latency: End-to-end processing latency (event time to processing time)
- Lag: Consumer lag behind producer
- State Store: State store size and growth rate
- Errors: Processing errors and dead letter queue rates
JMX Metrics Collection
// Enable JMX metrics
props.put(StreamsConfig.METRICS_RECORDING_LEVEL_CONFIG, "INFO");
props.put(StreamsConfig.BUILT_IN_METRICS_VERSION_CONFIG, "latest");
// Custom metrics
StreamsMetrics metrics = streams.metrics();
metrics.addLatencyRateTotalSensor("topology", "process", "record-process",
Sensor.RecordingLevel.INFO, "node-id");
Best Practices and Recommendations
Design Guidelines
- Keep transformations simple: Break complex logic into smaller, testable components
- Minimize state: Use stateless operations when possible for better scalability
- Plan for schema evolution: Use compatible serialization formats like Avro or Protobuf
- Design for reprocessing: Make operations idempotent and support replay scenarios
- Monitor data quality: Implement data validation and quality checks in topology
Topology Testing
// Unit testing with TopologyTestDriver
@Test
public void shouldProcessOrders() {
TopologyTestDriver testDriver = new TopologyTestDriver(topology, props);
TestInputTopic<String, OrderEvent> inputTopic =
testDriver.createInputTopic("orders", stringSerde.serializer(), orderSerde.serializer());
TestOutputTopic<String, ProcessedOrder> outputTopic =
testDriver.createOutputTopic("processed-orders", stringSerde.deserializer(), processedOrderSerde.deserializer());
inputTopic.pipeInput("key1", createOrderEvent());
ProcessedOrder result = outputTopic.readValue();
assertThat(result.getStatus()).isEqualTo("PROCESSED");
testDriver.close();
}
Deployment Considerations
- Use container orchestration (Kubernetes) for automatic scaling and failover
- Implement proper health checks and graceful shutdown procedures
- Plan for data migration strategies during topology changes
- Set up proper alerting for critical metrics and error conditions
- Consider multi-region deployment for disaster recovery
FAQ
Common questions
Discussion
Questions & comments · 0
Sign In Sign in to leave a comment.