Written by
Halkwinds Editorial Team
Halkwinds Research & Editorial

Streaming Data Architecture: Apache Kafka, Flink, and Real-Time Pipelines
How to design and operate streaming data pipelines for high-volume, low-latency use cases — from ingestion to serving.
If you have ever tried to bolt real-time analytics onto a batch pipeline, you already know the pain: overnight jobs that arrive too late to matter, brittle cron chains, and dashboards that lag reality by hours. Streaming data architecture flips this model. Instead of moving data in large periodic batches, you treat data as an unbounded, continuously flowing stream — processing events milliseconds to seconds after they happen. This article is a practical guide for data engineers building high-volume, low-latency pipelines with Apache Kafka and Apache Flink, covering the concepts, the implementation choices, and the operational realities that documentation rarely mentions.
- Background / Why This Matters
- Core Concepts and Architecture
- Implementation Strategy
- Scaling and Operational Considerations
- Common Mistakes / What to Avoid
- Frequently Asked Questions
- Conclusion
Background / Why This Matters
The shift from batch to streaming is driven by use cases where latency directly affects value: fraud detection, dynamic pricing, real-time inventory, personalization, IoT telemetry, and operational monitoring. A recommendation that arrives 30 minutes after a user leaves the site is worthless. A fraud alert that fires after the money has moved is just an audit log.
Batch systems assume the world holds still while you process it. Streaming systems assume the world never stops. That assumption cascades into every design decision — how you handle late-arriving data, how you recover from failures, how you guarantee correctness, and how you scale under bursty load.
Why streaming data architecture with Kafka specifically? Kafka has become the de facto durable, replayable log at the center of most streaming platforms. It decouples producers from consumers, buffers backpressure, and lets multiple downstream systems consume the same data independently. When paired with a stateful stream processor like Apache Flink, you get an end-to-end pipeline capable of exactly-once semantics at scale.
Actionable takeaway: Before adopting streaming, write down the latency requirement in numbers. If "real-time" actually means "within 15 minutes," a micro-batch approach may be cheaper and simpler than a full streaming stack.
Core Concepts and Architecture
A production streaming pipeline typically has four layers: ingestion, transport, processing, and serving.
1. Ingestion
Events originate from application code, change-data-capture (CDC) tools like Debezium, IoT gateways, or clickstream collectors. The goal is to get events into Kafka reliably. Use idempotent producers (enable.idempotence=true) and set acks=all to avoid silent data loss.
2. Transport: Apache Kafka as the log
Kafka organizes data into topics, which are split into partitions. Partitions are the unit of parallelism and ordering — Kafka guarantees order within a partition, not across them. Your partition key (e.g., user_id or account_id) determines both parallelism and where ordering matters. Choose it deliberately, because repartitioning a live topic is painful.
3. Processing: Apache Flink vs. Spark Streaming
This is where the transformation happens: filtering, enrichment, windowed aggregation, joins, and pattern detection. The two dominant open-source options are Apache Flink and Spark Structured Streaming. They take fundamentally different approaches.
| Dimension | Apache Flink | Spark Streaming |
|---|---|---|
| Processing model | True event-at-a-time streaming | Micro-batch (Continuous mode experimental) |
| Typical latency | Milliseconds to low seconds | Hundreds of ms to seconds |
| State management | Native, RocksDB-backed, mature | Improving, but historically weaker |
| Event-time & watermarks | First-class, flexible | Supported, less granular control |
| Best fit | Low-latency, stateful, CEP | Teams already invested in Spark/batch |
Rule of thumb: If latency and complex stateful logic are your priorities, choose Flink. If your team already runs Spark for batch and your latency tolerance is a few seconds, Spark Structured Streaming reduces cognitive overhead by unifying batch and streaming code.
4. Serving
Processed results land somewhere queryable: a low-latency key-value store (Redis, Cassandra), an OLAP engine (Apache Pinot, ClickHouse, Apache Druid), a search index (Elasticsearch), or back into Kafka for downstream consumers.
Event time vs. processing time
The single most important concept in streaming is the distinction between event time (when the event actually occurred) and processing time (when your system saw it). Networks lag, mobile devices go offline, and events arrive out of order. Flink's watermarks let you reason about event time and decide how long to wait for late data before closing a window.
Actionable takeaway: Always aggregate on event time for correctness, and configure explicit allowed-lateness. A watermark strategy of "bounded out-of-orderness of 5 seconds" is a sane default to tune from.
Implementation Strategy
A pragmatic build sequence avoids the trap of designing the perfect platform before shipping any value.
- Start with one high-value stream. Pick a single use case — say, real-time order events — rather than migrating everything. Prove the pattern end to end.
- Model your topics and schemas first. Use a schema registry (Confluent Schema Registry or Apicurio) with Avro or Protobuf. Enforce backward compatibility so producers and consumers can evolve independently. This one decision prevents entire categories of production incidents.
- Design partition keys around ordering and cardinality. High-cardinality keys spread load evenly; a low-cardinality key (like
country) creates hot partitions. - Build the Flink job with checkpointing enabled. Configure checkpoints (e.g., every 30–60 seconds) to durable storage like S3. This is what enables fault-tolerant recovery.
- Choose your delivery guarantee. Exactly-once end-to-end requires idempotent Kafka producers, Flink's two-phase-commit sink, and a sink that supports transactions. It costs latency and throughput, so only pay for it where duplicates cause real harm (billing, inventory).
A minimal reference pipeline
A typical first pipeline looks like: application → Kafka topic (partitioned by customer_id) → Flink job (enrich with reference data via a broadcast state or async lookup, aggregate over 1-minute event-time windows) → sink to ClickHouse and a Kafka output topic for alerts. This handles enrichment, windowing, and fan-out — the three things nearly every streaming use case needs.
Designing this correctly the first time is harder than it looks, and mistakes compound in production. This is precisely the kind of work Halkwinds' Data & Analytics practice handles — architecting Kafka and Flink pipelines that survive real traffic rather than demo traffic.
Actionable takeaway: Enforce schemas from day one. Retrofitting a schema registry onto a running platform with dozens of producers is one of the most expensive migrations you can face.
Scaling and Operational Considerations
Streaming systems fail differently from batch systems. A batch job that breaks can be rerun; a streaming job that falls behind accumulates backlog and consumer lag in real time.
Monitor the right signals
- Consumer lag — the gap between the latest offset and your consumer's position. This is your single most important health metric. Rising lag means you are falling behind reality.
- Checkpoint duration and failure rate in Flink. Growing checkpoint times often signal state that is too large or a slow state backend.
- Backpressure — Flink's UI exposes which operator is the bottleneck. Address the slowest stage, not a random one.
- Watermark progression — if watermarks stall, windows never fire and output silently stops.
Scaling levers
Throughput in Kafka scales with partitions, but you cannot easily reduce partition count later, and more partitions increase broker overhead and rebalance times. Start with enough headroom (a common heuristic is targeting a few MB/s per partition) but avoid tens of thousands of partitions per cluster. In Flink, scale via parallelism, and use reactive mode or Kubernetes autoscaling to adjust to load. Note that rescaling stateful Flink jobs requires redistributing state, which is why savepoints matter.
State and storage
Large keyed state (RocksDB) needs fast local disks and generous memory. Research and community reports suggest that most streaming production incidents trace back to state growth, unbounded windows, or GC pressure — not the streaming framework itself. Set TTLs on state you don't need forever.
Actionable takeaway: Alert on consumer lag trend, not just absolute value. A lag of 10,000 messages that is shrinking is fine; a lag of 500 that is growing every minute is an incident.
Common Mistakes / What to Avoid
- Treating "real-time" as a religion. Not every dataset needs sub-second latency. Streaming adds operational complexity — use it where latency has business value.
- Ignoring event time. Aggregating on processing time produces subtly wrong results whenever data arrives late, which in the real world is always.
- No schema governance. A producer that changes a field type silently corrupts every downstream consumer. Use a schema registry with compatibility enforcement.
- Poor partition key choice. Hot partitions cap your throughput no matter how many resources you throw at the cluster.
- Unbounded state. Joins and windows that never expire eventually exhaust memory and crash the job. Always set retention and TTL.
- Skipping the dead-letter path. Malformed events will arrive. Without a dead-letter topic, one bad record can stall or crash a consumer.
- Chasing exactly-once everywhere. It has real cost. Many pipelines are perfectly served
Explore Further