Deep Read · DDIA · Chapter 11
Designing Data-Intensive Applications · Ch 11 · Martin Kleppmann · 2017
The "purchase alert" that pops the instant you tap your card, the little car sliding across your ride-hailing map, the viewer count ticking up in a live stream—none of these can "wait until tonight to compute." Last chapter's batch processing is like washing one big tub of laundry: you collect a full load before running it, so what you see is always "yesterday's world." This chapter is about a different kind of work: process each piece of data the moment it arrives, so the result always hugs "right now." That's stream processing.
Picture every thing that happens—a click, a payment, a sensor reading—as a little sticky note you write once and never change: "at this time, this person did this." Stream processing is standing next to a conveyor belt of these notes as they keep flying in, reading each one as it comes, updating the books and firing whatever alert is due on the spot—rather than waiting for a whole sack to pile up before opening it.
Batch processing has one incurable flaw: it's always waiting—waiting for a batch to fill, waiting for the whole batch to finish—so what you get is always the stale books from hours ago. But plenty of things in real life can't wait: fraud must be blocked on the spot, flash-sale inventory watched live, an outage flagged this instant. You can't recompute an entire day's data from scratch just to glance at the present moment.
Two plain ideas hold up stream processing. First, a never-deleted ledger. Don't toss each sticky note after reading it—pin them one after another onto a long conveyor belt and never tear them off; whoever wants to read brings their own bookmark, notes where they've reached, and can rewind the bookmark to re-read anytime (this is exactly what Kafka does). Second, the ledger and the balance are the same thing. Your bank balance is nothing but the sum of every transaction replayed from the start; conversely, as long as you keep every transaction, the balance at any moment can be recomputed. "Store every change, not just the latest state"—that small pivot is the soul of the chapter.
With this replayable ledger, a change in one place can fan out to everyone who needs it within seconds: the moment the database changes, the search index, cache, and reports update live; fraud detection finishes deciding before your finger leaves the screen. Use streaming when you need it "on demand right now"; leave "run tonight, out by morning" work to batch. One honest cost: the moment you chase real-time, "time" itself gets slippery—a message sent from the subway may arrive minutes late; out-of-order and late events mean "what exactly happened this minute" never has a hard deadline, only a rough line you draw while admitting a few will slip through.
Stream processing = process each item as it arrives, keeping results glued to the present, for the sake of "on demand right now." Two pillars: a never-deleted, replayable ledger (Kafka), and "store every change, not just the latest state"—the balance is just the sum of the ledger. The cost: real-time makes "time" slippery; out-of-order and late events have no hard deadline.
Want CDC, stream-table duality, windows & watermarks, exactly-once, and diagrams? → Switch to Deep mode
This chapter is about stream processing: treat data as a never-ending, unbounded stream of events, process each event as it arrives, and keep derived results (indexes, caches, live metrics, alerts) continuously close to "now." It shares last chapter's worldview—immutable data + derivation—only swapping "one bounded batch" for "an unbounded continuous stream." Two pillars hold up the whole chapter: the log-based message broker (e.g. Kafka)—a durable, ordered, replayable event log; and event sourcing / stream-table duality—taking "every change" rather than "the latest state" as the source of truth. One counterintuitive line: you'd think the hard part of streaming is "going fast"—it's actually time: the mismatch between event time and processing time is the real deep water of this chapter.
(user A, 12:03, clicked item X). Streaming is all about moving events around.This chapter is the second piece of Part III, "Derived Data," right after batch processing (Ch10). Ch10 handles bounded, static batches; this chapter turns to unbounded, real-time streams. The two aren't rivals but two faces of one worldview—both treat data as immutable input and derive results from it; the only difference is "batch vs stream." It builds on Ch10's batch processing and cashes in setups from Ch5 (replication logs) and Ch7 (transactions & exactly-once); it leads into the final Ch12, where the author reorganizes the whole data system around streams. It maps to the real-world Kafka / Flink / Spark Streaming ecosystem, real-time warehouses, and fraud and monitoring pipelines.
Suppose you're building fraud detection for e-commerce: 50,000 transactions pour in per second, and between the user tapping "pay" and the bank replying—under 200 milliseconds—you must judge whether this one is fraudulent. Suppose Ops also wants a live dashboard showing this-minute GMV and per-city order heat. What these have in common: the value of the result decays fast with time. "Run tonight, out by morning" batch is useless here—by the time you finish, the money's stolen and the sale is over.
Yet you can't just shrink batch's "recompute all data from scratch every hour" down to "every second"—that would crush your machines and database. The real problem is three interlocking challenges: ① how to deliver an endless flow of events reliably and in order from producer to processor, and, after the processor crashes and restarts, resume from where it left off—or even replay history; ② how to propagate a change in one place (one row edited) in real time and correctly to every derived system (search, cache, reports) so they don't contradict each other; ③ how to give a trustworthy aggregate over "one time window" in a world where events arrive out of order and late. Solve none of these and you're stuck with either hours of latency or a pile of fragile real-time scripts that drop messages and never reconcile.
Streaming starts with how to get events from producer to consumer. The naïve way—have consumers poll a database for new data—is clumsy. Dedicated message brokers emerged, historically in two schools; understanding their difference is the key to why Kafka matters:
The point of the log-based broker: it has both a database's durability and a message queue's low latency—fusing "delivering messages" and "storing data" into one thing. That's the technical bedrock that lets a "stream" serve as a trustworthy source of truth. The cost: one partition can only be read in order by a single consumer, so when you want 100 consumers to share one batch of messages in parallel and don't care about order, a traditional queue is actually more flexible.
Wiring the "stream" abstraction to a database unlocks the chapter's most useful class of applications: keeping all systems in sync. In practice you almost never use just one database—beyond the primary there's a search engine (Elasticsearch), cache (Redis), warehouse, feature store, all of which must track the primary. The old way is application-level dual writes (update the DB, then manually update search), but dual writes easily fail to reconcile under concurrency or partial failure. The cleaner way is to turn database changes into a stream:
Change data capture (CDC): every insert/update/delete on the primary is captured as an event (typically by reading its replication log / WAL) and pushed onto a stream. Every downstream subscribes and applies it in order, ending up eventually identical to the primary—essentially generalizing Ch5's leader–follower replication across heterogeneous systems. Tools like Debezium do exactly this.
Event sourcing: a more radical step—don't treat "current state" as the source of truth; treat "every event that led to the state" as the source of truth. A shopping cart stores not "2 items now" but the immutable event chain add X, add Y, remove X; current state is computed by replay. Benefits: full audit (every step traced, accountable), time-travel to any moment, and many different views derived from one event stream (the same order events can drive inventory, revenue, and recommendations).
Behind CDC and event sourcing is one deep observation the author calls the duality of streams and tables:
This is no word game but an architectural worldview: turning the database inside out—a traditional database hides the "changelog" internally and exposes only "current state"; streaming reverses it, promoting the changelog itself to a first-class citizen (right there in Kafka) and demoting "current state" to a rebuildable, multiply-copied materialized view. Search indexes, caches, and reports all become "different projections of one source-of-truth log"—inherently consistent, rebuildable from the log when broken. This is Kleppmann's famous "turning the database inside out," and the intellectual core of the final Ch12.
This is the acknowledged brain-bender—and the true test—of the chapter. In batch, "time" isn't a problem (all data is there, compute freely); in streaming, the moment you want "how many transactions in the past minute," you hit a philosophical knot: which "minute"?
The two often disagree: a user places an order in the subway, has no signal, and 5 minutes later—leaving the station—their phone finally sends the event. By processing time, it's wrongly counted into the window 5 minutes later. Processing time is simple but distorted by system load and network jitter; event time is "correct" but hits a deadlock: you can never be sure all events of a given window have arrived—a straggler could always be on the way.
The remedy is the watermark: the system maintains a heuristic line, "events before event time T have, I believe, mostly arrived," and once the watermark passes a window's end boundary, it emits that window's result. What about late events arriving afterward? Three attitudes: drop them outright (and count/alert), issue a correction (retract the old result and re-emit), or allow a grace period and drop only stragglers beyond it. There's no free lunch here—wait longer to miss fewer and you sacrifice latency; emit sooner and you tolerate the occasional straggler.
Cutting windows has its own varieties, four common ones: tumbling—end-to-end, non-overlapping ("each whole minute"); hopping—fixed length but sliding by a smaller step, so they overlap ("every minute, count the past 5 minutes"); sliding—at any instant look "back N minutes"; session—no fixed length, cut by "a user's burst of activity, broken only after some minutes of silence."
As in batch, streams often need joins, but because data is "in motion," the shapes are subtler—three kinds: ①stream-stream join (windowed)—pair two streams within a time window, e.g. match "search events" with "click events" for click-through rate; the hard part is both sides move, so you must buffer a window of state waiting for the other; ②stream-table join (enrichment)—use a (slowly changing) table to enrich each event in a stream, e.g. attach a user profile to each click; implementations often turn the table into a stream via CDC too, maintaining a local copy; ③table-table join (maintaining a materialized view)—merge two changelog streams into a continuously updated joined view.
Fault tolerance and exactly-once is streaming's ultimate exam. A failed batch job just reruns the whole batch (input's still there); a stream is unbounded, never "finishes"—so after a crash-restart, how do you guarantee "each event's effect takes hold exactly once, no dupes, no drops"? Three mainstream routes: ①microbatching (Spark Streaming)—slice the stream into one-second mini-batches and degrade to batch for fault tolerance; simple, but latency is bounded by batch length; ②checkpoint + idempotence (Flink)—periodically snapshot operator state; on crash, roll back to the last checkpoint and replay, with idempotent output, achieving "effectively-once"; ③atomic commit—bind "advance read progress" and "write result" into one transaction that succeeds or fails together (e.g. Kafka transactions). Note: exactly-once means "the external effect takes hold exactly once," not "physically processed only once"—underneath it's still usually replay + dedup / idempotence.
Streaming choices trade off replayability, ordering, real-timeness, and correctness cost. Three tables pull out the most-tested, most-useful comparisons.
Table 1 · Traditional message queue vs log-based broker (Kafka)
| Traditional queue (AMQP/JMS) | Log-based broker (Kafka) | |
|---|---|---|
| After consume | ack then delete, one-shot | append-only, kept after read, retained by time/capacity |
| Replay? | No—gone once consumed | Yes—rewind offset to re-read history |
| Ordering | Easily scrambled under concurrency | Strict order within a partition |
| Scaling | Consumers share one queue (load balance) | Partition by key; one consumer per partition, parallel across |
| Best at | Task dispatch (handle once, e.g. send email) | Event streams / data sync / replay & many downstreams |
Table 2 · Event time vs processing time (and late-event handling)
| By processing time | By event time | |
|---|---|---|
| Meaning | When the event was processed | When the event actually happened |
| Difficulty | Simple, no waiting | Hard—needs watermarks + buffered state |
| Correctness | Distorted by load / network jitter | Semantically correct, reflects real business moment |
| Late events | Miscounted into a later window | Arrive past watermark → drop / correct / grace period |
| When to pick | Coarse monitoring of "system throughput rhythm" | Books of "real business moment" (billing, fraud, analytics) |
Table 3 · Choosing among the three stream joins
| Type | What it does | Typical use | Difficulty / cost |
|---|---|---|---|
| Stream-stream (windowed) | Pair two streams within a time window | Search × click events for CTR | Both sides move; must buffer the whole window's state |
| Stream-table (enrichment) | Enrich each stream event with a table | Attach user profile to a click | Needs a local table copy (often synced via CDC) |
| Table-table (materialized view) | Merge two changelogs into a live joined view | Maintain a "user+order" joined view over time | Large state; table changes must propagate promptly |
Stream processing is the skeleton of today's real-time data stack. Apache Kafka (open-sourced by LinkedIn in 2011) elevated "the log" into infrastructure and became a de facto standard for event streams; atop it grew Kafka Streams, Apache Flink, Spark Streaming, Storm, Samza. CDC tools (Debezium) wire legacy databases into streams; real-time warehouses, real-time fraud detection, recommendation features, monitoring alerts, and IoT telemetry are all, at heart, this chapter's ideas. The author's "turning the database inside out" (unbundling / stream-table duality) directly frames Ch12's direction. In interviews, "why can Kafka be both durable and low-latency," "how is exactly-once actually implemented," and "event time vs processing time and watermarks" are high-frequency questions for data / backend roles—the answers are all here.
① One line: stream processing = treat data as an unbounded event stream, process each as it arrives, keeping derived results close to now—for "on demand right now."
② Two faces of one coin with batch: same "immutable input + derivation," differing only in one bounded batch vs an unbounded stream.
③ Pillar one: the log-based broker (Kafka)—append-only, ordered, kept after read, replayable via the offset bookmark; it has durability and replay a "sign-and-delete" queue lacks.
④ Pillar two: CDC + event sourcing—take "every change" not "latest state" as source of truth; one stream keeps search/cache/warehouse eventually consistent with the primary, replacing fragile dual writes.
⑤ Soul: stream-table duality—a table is a snapshot of a stream (replay + sum), a stream is a table's changelog; "turn the database inside out," making state a view rebuildable from the log.
⑥ Hardest is time: event time vs processing time; use windows + watermarks to decide when a window closes; late events can only be dropped / corrected / graced—no having both.
⑦ Three joins (stream-stream windowed, stream-table enrichment, table-table view); exactly-once means "effect exactly once," via microbatch / checkpoint+idempotence / atomic commit.
⑧ In practice: Kafka → Flink / Spark Streaming / Kafka Streams, powering real-time warehouses, fraud, monitoring; Kreps's "The Log" and Google's Dataflow Model are the sources, leading straight into the final Ch12.