Skill

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.

Works with apache kafkaapache stormapache flinkpulsar

Maintainer of this project? Claim this page to edit the listing.


91
Spark score
out of 100
Updated 7 months ago
Version 1.0.0
Models

Add 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

01

Design logical and physical stream processing topologies.

02

Implement state management, windowing, and exactly-once semantics.

03

Optimize performance, parallelism, and resource management.

04

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

  1. Keep transformations simple: Break complex logic into smaller, testable components
  2. Minimize state: Use stateless operations when possible for better scalability
  3. Plan for schema evolution: Use compatible serialization formats like Avro or Protobuf
  4. Design for reprocessing: Make operations idempotent and support replay scenarios
  5. 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.