Engineering Atlas › System Design Fundamentals

Streaming-only data pipelines

12 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

The log as the system of record

Every event is appended to a partitioned, ordered log. Retention is long: days to forever, using tiered storage that moves old segments to object storage, or log compaction for keyed changelogs where only the latest value per key matters. The two do not mix: Kafka's tiered storage, declared production-ready in 3.9 (Kafka 3.9 release), still does not support compacted topics, so compacted changelogs stay on broker disks (Kafka docs). Consumers track their position as an offset per partition, so any consumer can rewind.

Jay Kreps named the pattern in 2014 as an answer to Lambda's two codebases: keep the raw input in the log, and recompute by running a second copy of the same stream job (Kreps). The Azure Architecture Center frames it the same way, with events mirrored to long-term storage so the stream can be recomputed when needed (Azure).

Stateful stream jobs

A job reads the log, maintains state (counters, windows, join tables) in a local store such as RocksDB, and writes results to an output: another topic, a database, or a search index. State is checkpointed with the input offsets, so after a crash the job restores state and resumes from the matching offset without losing or double counting.

Reprocessing instead of batch

When logic changes, you do not patch the old output in place. You deploy job v2 alongside v1, reading from offset zero (or from a snapshot point), writing to a new table views_v2. v2 processes history much faster than real time because it is reading stored data in bulk. When v2's lag reaches near zero, readers switch to views_v2 and v1 is stopped. Rollback is switching back.

Replaying from offset zero is not the only start point. If the change is small and the state is compatible, v2 can start from a savepoint of v1, a consistent image of its state and offsets. In Flink this only works if every stateful operator has a stable ID set with uid(); auto-generated IDs change when the program changes, and the restore fails (Flink docs). A logic bug that corrupted the state itself still needs a replay from before the bug.

Event time and ordering

Because historical replay races through months of data in hours, the job must use event time (when the event happened) and watermarks, not processing time (when the job sees it), to assign events to windows (Akidau). Otherwise a replay assigns every event to "now". Ordering is guaranteed only within a partition, so keys that need ordered processing must share a partition.

Down the stack

Layer 1The contract

The log promises that records within a partition are stored and delivered in offset order, that acknowledged records (with acks=all and min.insync.replicas=2) survive the loss of one broker, and that records remain readable until retention expires. acks=all and idempotence are producer defaults today (producer configs), but min.insync.replicas must still be raised from its default of 1 for that promise to hold. A consumer group promises each partition is assigned to one consumer at a time, and committed offsets mark where processing resumes. Together these let a job promise that its output is a deterministic function of the log prefix it has processed.

Layer 2The mechanism

Each Kafka partition is a directory of segment files, by default 1 GiB each (log.segment.bytes, Kafka configs), named by their base offset, with a sparse .index mapping offsets to byte positions and a .timeindex mapping timestamps to offsets. A consumer that seeks to offset 0 or to a timestamp uses these indexes to find the right segment and byte position in a binary search. With tiered storage, closed segments are copied to object storage and deleted locally after a short local retention; brokers fetch remote segments on demand when an old offset is requested. A fetch request serves remote data for only one partition at a time, which caps how fast one consumer can replay many partitions from the remote tier (Kafka wiki). Log compaction runs a cleaner thread that rewrites old segments keeping only the newest record per key, and treats a record with a null value (a tombstone) as a delete. Tombstones themselves are removed after delete.retention.ms, 24 hours by default, so a consumer rebuilding from offset 0 must reach the head within that time or it will miss deletes (Kafka design). Consumer offsets are themselves stored in the compacted internal topic __consumer_offsets, keyed by group, topic, and partition.

Layer 3The machine

Kafka relies heavily on the OS page cache. Producers append to the active segment with sequential writes, and recent reads are served from page cache by sendfile, which copies bytes from the cache to the socket inside the kernel without passing through user space (Kafka design). A live consumer reading the tail therefore costs almost no disk I/O. A replay is different: it reads cold segments from disk or object storage, pulling gigabytes through the page cache and evicting the hot tail that live consumers depend on. Their reads then fall through to disk, latency rises from sub-millisecond to tens of milliseconds, and producers can slow down too. This is the physical reason to throttle replays with quotas or to serve them from tiered storage instead of broker disks.

Why it's built this way

  1. Maintaining two codebases for batch and stream is expensive, so Kappa keeps one.
  2. One codebase needs a way to recompute history, so the log is retained long enough to replay.
  3. Disks and networks are efficient for large sequential reads, so replaying a log is much faster than re-querying a database row by row.
  4. Keeping years of segments on broker SSDs is costly, so old segments move to object storage, and keyed state uses compaction.

Diagram

flowchart LR
  P["Producers"] --> LOG["Kafka log (long retention)"]
  LOG -->|"live offsets"| J1["Job v1"]
  LOG -->|"replay from offset 0"| J2["Job v2"]
  J1 --> T1["Table views_v1"]
  J2 --> T2["Table views_v2"]
  T1 --> SW{"Read alias"}
  T2 -.->|"switch when caught up"| SW
  SW --> APP["Applications"]

Worked example

A marketplace computes seller metrics (orders, revenue, cancellation rate by day) from an order_events topic with 64 partitions. Traffic averages 20,000 events per second at 500 bytes, about 860 GB per day. Kafka keeps 7 days on broker disks and 2 years in tiered object storage, around 630 TB, costing roughly $14,000 per month at standard object storage prices.

The team discovers the cancellation rate double counts orders cancelled and then reinstated. They fix the logic and launch v2 from the start of retention.

Replay throughput: each of 64 Flink task slots reads one partition. Reading from tiered storage and updating RocksDB, each sustains about 40 MB/s, so the job reads 2.5 GB/s in aggregate. Two years of data, 630 TB, would take about 70 hours. That is too long. The metric is only displayed for 13 months, so v2 replays from 13 months ago, about 340 TB, in roughly 38 hours. Partition count caps read parallelism at 64, so going faster would mean replaying from a Parquet archive in the lake instead.

v1 keeps serving during replay. When v2's lag drops under a minute, the alias moves; v1's table stays a week for rollback.

In the wild

Trade-offs

OptionStrengthsWeaknessesChoose when
KappaOne codebase and semantics; reprocessing via replayLong log retention cost; replay time for large historiesLogic is incremental and the log is the source of truth
LambdaBatch can use heavy, non-incremental logicTwo implementations that driftBatch-only computations are essential
Kappa with lake snapshotsReplay from a recent state snapshot plus log tailSnapshot and log must stay consistentHistories too large to replay from zero
Batch onlyCheap and simpleNo freshnessMinutes or hours of latency are acceptable

Pitfalls in production

Leadership lens

Check yourself

  1. Why must Kappa jobs use event time rather than processing time?

    Show answer

    During replay, processing time is the moment of reprocessing, so windows and aggregations would be wrong; event time keeps results identical to a live run.

  2. How does Kappa handle a logic bug that affected three months of output?

    Show answer

    Deploy a fixed job version that replays the log from before the bug into a new output, then switch readers once it has caught up.

  3. What makes log compaction useful for Kappa?

    Show answer

    For keyed changelogs it keeps the latest value per key indefinitely, so new jobs can rebuild current state without storing every historical update.

Sources and further study

Primary

Systems docs

Books

Other curricula

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →