Real-Time Data Streaming Architecture

Build production streaming systems. Covers Kafka, Flink, Kinesis, event schema design, exactly-once processing, stream-table duality, windowing, and backpressure management.

This is an architecture pattern for systems that process data as it arrives, rather than in batches. It applies when data must be consumed within milliseconds or seconds of being generated, typically for event-driven applications, real-time dashboards, or immediate alerting.

Setting up a Stream Processor

Start a basic stream processing node

Use a standard consumer group with a message broker like Apache Kafka to consume from topics:

kafka-console-consumer.sh \
  --bootstrap-server kafka-broker:9092 \
  --topic user-events \
  --group event-processor \
  --from-beginning

The key flag is --group, which defines the consumer group membership. Each consumer in the same group receives a partition assignment, and consumers in different groups process the same messages independently.

Configure consumer offsets

Set offset management strategy via consumer configuration:

# consumer.properties
group.id=event-processor
enable.auto.commit=true
auto.commit.interval.ms=5000
session.timeout.ms=30000
heartbeat.interval.ms=3000

The enable.auto.commit flag controls whether the system automatically records offsets after each poll. When set to false, you must manually commit offsets using commitSync() or commitAsync().

Handle partition rebalancing

A consumer group can trigger a rebalance when new consumers join or leave. During this process, partitions are reassigned, and processing may pause until the new assignment is complete.

Consuming from Multiple Sources

Connect to multiple topics with one consumer

Use a single consumer instance to subscribe to several topics:

kafka-console-consumer.sh \
  --bootstrap-server kafka-broker:9092 \
  --topic user-events,user-actions \
  --group unified-processor

The --topic flag accepts comma-separated topic names. A consumer can only belong to one group, so it processes messages from all subscribed topics.

Set up a consumer with custom deserializers

Use --value-deserializer and --key-deserializer flags for structured data:

kafka-console-consumer.sh \
  --bootstrap-server kafka-broker:9092 \
  --topic user-events \
  --group event-processor \
  --value-deserializer=org.apache.kafka.common.serialization.StringDeserializer \
  --key-deserializer=org.apache.kafka.common.serialization.IntegerDeserializer

Deserialization errors silently fail unless you configure error handling. When deserialization fails, messages are skipped and logged.

Streaming Data to a Sink

Send processed data to another system

Use kafka-console-producer.sh to forward messages:

kafka-console-producer.sh \
  --broker-list kafka-broker:9092 \
  --topic enriched-events \
  --property parse.key=true \
  --property key.separator=:

The --property parse.key=true flag indicates that input data contains keys separated by the key.separator.

Configure delivery guarantees

Set producer acknowledgment level for reliability:

# producer.properties
acks=1
retries=3
batch.size=16384
linger.ms=5

With acks=1, the producer waits for the leader to write messages before considering them acknowledged. With acks=0, it sends messages without waiting, which can cause data loss.

Handling Backpressure and Failure States

Monitor consumer lag

Use Kafka's built-in metrics:

kafka-consumer-groups.sh \
  --bootstrap-server kafka-broker:9092 \
  --describe \
  --group event-processor

This command shows Lag for each partition, indicating how many messages behind the consumer is. A growing lag signals potential performance issues or system overload.

Implement circuit breaker on failure

Use a standard HTTP client with timeout configuration:

curl -X POST http://sink-service:8080/events \
  --connect-timeout 5 \
  --max-time 10 \
  --data-binary @- \
  --header "Content-Type: application/json"

If the sink service does not respond within the timeout, the stream processor fails the message and retries with exponential backoff.

Handle consumer group failures

When a consumer dies unexpectedly, Kafka triggers a rebalance. If this happens too frequently, you'll see repeated Rebalance started messages in logs.

Use the following to check group health:

kafka-consumer-groups.sh \
  --bootstrap-server kafka-broker:9092 \
  --describe \
  --group event-processor \
  --verbose

This includes Consumer ID and Host fields that help identify problematic consumers.

Configuring Stream Processing Logic

Process data in a streaming pipeline

Use a simple Kafka Streams application:

StreamsBuilder builder = new StreamsBuilder();
KStream<String, String> source = builder.stream("user-events");
KStream<String, String> processed = source.mapValues(value -> value.toUpperCase());
processed.to("processed-events");

The mapValues() operation applies a transformation to each message. If a transformation throws an exception, the stream stops processing until it is restarted.

Configure stream state stores

Define a RocksDB-based state store:

KTable<String, Long> counts = builder.table(
  "user-events",
  Materialized.<String, Long>as("event-counts")
    .withKeySerde(Serdes.String())
    .withValueSerde(Serdes.Long())
);

State stores are local to the application and persist data in memory or on disk. When a state store becomes corrupted, it may cause a full restart of the processing pipeline.

Managing Data Flow and Throughput

Tune consumer polling rate

Adjust poll interval and batch size:

# consumer.properties
fetch.min.bytes=1024
fetch.max.wait.ms=500
max.poll.records=1000

The fetch.min.bytes parameter ensures that a consumer waits for at least 1024 bytes of data before returning from a poll. This reduces the number of small fetches and improves throughput.

Set up rate limiting

Use HTTP headers to control message rate:

curl -H "X-RateLimit-Limit: 100" \
     -H "X-RateLimit-Remaining: 99" \
     http://sink-service:8080/events

A sink that does not implement rate limiting can become overwhelmed and drop messages. If a sink returns a 429 status code, the stream processor should retry with backoff.

Monitoring and Observability

Track message throughput

Monitor Kafka metrics using JMX:

jconsole kafka-broker:9999

Check Consumer and Producer MBeans for lag and throughput. Look for records-lag-max in consumer metrics to detect slow consumers.

Set up alerting on lag

Query metrics from a monitoring system like Prometheus:

max(kafka_consumer_fetch_manager_records_lag_max{group="event-processor"})

If this value exceeds 1000 messages, trigger an alert. Lag can accumulate over time and indicate processing bottlenecks.

Failure Modes and Silent Failures

Consumer lag accumulation

A growing consumer lag indicates that the processing rate is slower than the ingestion rate. This is not always visible in logs but becomes apparent through metrics.

Message duplication

When a consumer fails during processing, it may reprocess messages already handled. The enable.auto.commit=false setting requires manual offset management to prevent duplicates.

Serialization errors

If a message cannot be deserialized, it silently skips and logs the error. This is not an exception but a silent data loss event. Always validate serialization before deploying to production.

Producer timeout failures

A producer may timeout while waiting for acknowledgment from brokers. If retries are exhausted, messages are discarded.

State store corruption

State stores may become corrupted due to disk errors or memory issues. A corrupted store requires a full reset and replay of the stream. This can take hours depending on data volume.

Partition imbalance

If partitions are unevenly distributed across consumers, some consumers may be overloaded while others idle. Use kafka-reassign-partitions.sh to rebalance partitions manually.

Deployment Considerations

Scale consumer groups

To increase throughput, add more consumers to the same group:

kafka-console-consumer.sh \
  --bootstrap-server kafka-broker:9092 \
  --topic user-events \
  --group event-processor \
  --consumer-property num.stream.threads=4

Each consumer in a group processes one or more partitions. The number of consumers should not exceed the number of partitions for optimal performance.

Configure health checks

Use HTTP endpoints to expose readiness and liveness:

curl -f http://localhost:8080/health

A service that does not respond to /health is considered unhealthy and may be removed from a load balancer.

Handle configuration changes

When updating consumer or producer settings, restart the application. Running with incompatible settings causes data loss or processing errors. Always validate configurations before deployment.

Implement rollback strategy

If a new version of the stream processor fails, rollback to the previous version using:

kubectl rollout undo deployment/stream-processor

This preserves the last known good configuration and avoids partial failures.

This page was rewritten on 16 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.