Chapter 10 covered batch processing — bounded datasets, deterministic jobs, materialized intermediate state. Chapter 11 removes the bounded assumption. The input is unbounded: events arrive continuously, and you never finish processing. This changes everything about how you reason about time, joins, ordering, and fault tolerance.
The central insight: a stream is the derivative of a database, and a database is the integral of a stream. State is just an event log that has been accumulated over time. This idea connects change data capture, event sourcing, materialized views, and stream processing into a single framework.
Mission tie-in: System design interviews love streaming questions — "how would you design a real-time fraud detection system?", "how does Kafka work?", "how would you build a notification system?" This chapter also covers the Lambda Architecture debate, CDC (Debezium, Maxwell), and the conceptual foundation of Kafka, Flink, and Spark Streaming — all high-signal topics for data-intensive roles.
Batch processing assumes bounded input — a known, finite dataset. The job knows when it's done. MapReduce's sort step must read the entire input before producing any output (the last record might be the one with the lowest key). This is fundamentally incompatible with unbounded data.
You can fudge it by slicing time into chunks: process a day's worth of data every day, or an hour's worth every hour. But daily batch means 24-hour delay. Hourly means 1-hour delay. The logical conclusion is to process continuously — every event as it arrives. That's stream processing.
Stream = data incrementally made available over time. The concept already
exists everywhere: stdin/stdout, TCP connections, audio/video
streaming, lazy lists. This chapter treats event streams as a
data management mechanism — the unbounded counterpart to batch files.
An event is a small, self-contained, immutable object recording something that happened at a point in time. It contains a timestamp (according to some clock), and is encoded as text, JSON, or binary (Avro, Thrift — see Chapter 4).
| Batch concept | Stream equivalent |
|---|---|
| Input file | Topic / stream |
| Records | Events |
| Writing once | Producer (publisher) generates event |
| Multiple read jobs | Consumers (subscribers) process stream |
| Immutable files, rerunnable | Immutable events, replayable from log |
The naive approach: producers write to a database, consumers poll for new events. But polling is expensive when delays are low — most polls return nothing. Better to notify consumers when new events arrive.
A messaging system decouples producers from consumers. Two critical questions differentiate every system:
No intermediary: UDP multicast (financial tick data, low latency), ZeroMQ/nanomsg (brokerless pub/sub over TCP), StatsD (unreliable UDP metrics — approximate only), webhooks (HTTP callback). All assume producers and consumers are constantly online. If a consumer is offline, it misses messages (unless the producer buffers + retries, risking producer crash = data loss).
Centralized broker (RabbitMQ, ActiveMQ, IBM MQ, Google Cloud Pub/Sub). Producers and consumers connect as clients. Durability moves to the broker.
| Feature | Traditional DB | Message broker |
|---|---|---|
| Data retention | Until explicitly deleted | Deleted after delivery acknowledgment |
| Working set | Entire dataset (possibly large) | Short queues (fast) |
| Queries | Secondary indexes, snapshots | Topic subscription patterns |
| Change notification | Poll or trigger | Push on new message |
The two can be combined: multiple consumer groups, each doing fan-out within the group.
Consumers acknowledge processed messages so the broker can delete them. If the connection drops without acknowledgment, the message is redelivered to another consumer. Caveat: the original consumer may have fully processed the message but lost the acknowledgment in transit. Result: message reordering (Figure 11-2). Load balancing + redelivery inevitably reorders messages. Use separate queues per consumer if ordering matters.
JMS/AMQP treats messaging as transient — receiving + acknowledging a message deletes it. This is destructive: you can't rerun the same consumer and get the same result, and new consumers can't read past messages.
Log-based message brokers (Kafka, Kinesis, DistributedLog) combine database durability with message broker notification. The core data structure is an append-only log — exactly the same as the write-ahead log from Chapters 3 and 5.
tail -f model.| Aspect | JMS/AMQP | Log-based (Kafka) |
|---|---|---|
| Fan-out | Multiple consumers on topic | Independent consumers, each reads full log |
| Load balancing | Message-level: broker assigns | Partition-level: each consumer gets full partition |
| Delivery tracking | Acknowledge each message | Checkpoint consumer offset periodically |
| Message destruction | Deleted on acknowledge | Persistent until retention limit |
| Replayability | Can't replay (messages gone) | Seek offset back → replay entire history |
| Order preservation | Broken by redelivery | Strict within partition |
| Throughput | Degrades with queue growth (disk spilling) | Constant (always writing to disk) |
The consumer offset is like a database log sequence number (LSN). If a consumer fails, another picks up at the last recorded offset — may reprocess some messages (at-least-once). The offset is under consumer control, so replaying old messages is trivial: just reset the offset.
The log is a circular buffer on disk. A 6 TB drive at 150 MB/s sequential write holds ~11 hours of max-throughput data. In practice, deployments retain days or weeks of data. If a consumer falls behind and its offset points to a deleted segment, it misses messages — but only that consumer is affected.
The connection between databases and streams is fundamental: a replication log is a stream of database write events. State machine replication says: if every replica processes the same events in the same order, they end up in the same state.
Real applications need multiple data systems: OLTP database + cache + search index + data warehouse. Keeping them in sync via dual writes (app code writes to each system) has two problems:
The solution: make the database the single leader, and turn everything else into followers that consume its change stream.
CDC observes all data changes written to a database and exposes them as a stream. The database is the system of record; derived systems (search index, cache, warehouse) consume the change stream.
Event sourcing takes CDC's idea up one level of abstraction. Instead of capturing low-level state changes, the application logic is built on immutable events in an append-only log. Updates and deletes are discouraged.
| CDC | Event Sourcing |
|---|---|
| Records state changes (row updates) | Records user intent (domain events) |
| Application unaware of CDC | Application explicitly built on events |
| Log compaction possible (key-based) | Full event history needed (events don't override) |
| E.g., "row X updated to value Y" | E.g., "student cancelled enrollment" |
Commands vs Events: a request is a command (may fail validation). Once validated and accepted, it becomes a durable, immutable event. Consumers cannot reject events — by the time they see one, it's a fact.
Derive current state by replaying the event log (deterministic transformation). Snapshots optimize reads. The relationship: state = ∫ event stream, change stream = d(state)/dt.
The key realization: mutable state and an append-only event log are two sides of the same coin. The log is the truth; the database is a cache of the latest value per key (Pat Helland). This is exactly how accounting works: ledgers are append-only, and accounts are derived by summing transactions.
Three things you can do with a stream once you have it:
Search for patterns in event streams (1990s technology). Declarative queries specify event patterns; the engine maintains state machines. Reversed relationship: queries are stored long-term, events flow past them. Systems: Esper, IBM Streams, Apama, TIBCO StreamBase.
Aggregations over windows: rates, rolling averages, comparisons to previous intervals. Uses probabilistic algorithms: Bloom filters (set membership), HyperLogLog (cardinality), t-digest (percentiles). Systems: Storm, Spark Streaming, Flink, Samza, Kafka Streams, Google Cloud Dataflow.
Important: probabilistic algorithms are an optimization, not a necessity. Stream processing can be exact — don't assume approximation.
Stream of database changes → keep derived systems up to date. Unlike analytics, this needs a window that stretches back to the beginning of time (or log-compacted to full state). Samza and Kafka Streams support this via Kafka's log compaction.
This is the hardest part of stream processing. Two kinds of time:
Why processing time fails: queueing, network faults, restart, reprocessing of past events all create a gap between event time and processing time. Using processing time for windowing creates artifacts (Figure 11-7): a backlog replay looks like a traffic spike.
| Type | Duration | Overlap | Use case |
|---|---|---|---|
| Tumbling | Fixed | No | 10:03:00–10:03:59, 10:04:00–10:04:59 |
| Hopping | Fixed | Yes | 5-min window, 1-min hop: smoothing |
| Sliding | Fixed interval | Continuous | All events within 5 min of each other |
| Session | Variable | No | Group events by user, end after 30 min idle (sessionization) |
When do you declare a window complete? Events can arrive late (stragglers) due to buffering, network delays, or offline devices. Options:
Device clocks are untrusted. Mitigation: log three timestamps — (1) event occurred (device clock), (2) event sent to server (device clock), (3) event received (server clock). Subtract (2) from (3) to estimate clock offset, apply to (1).
Three types of stream joins, each with different state requirements:
Two activity event streams. Example: search events + click events for click-through rate. Maintain state: all search events in the last hour indexed by session ID, all click events in the last hour indexed by session ID. When a search event arrives, check for matching click. When a click arrives, check for matching search. Emit join results or "not clicked" expirations.
Challenge: events may arrive out of order (click before search). The window must be wide enough to accommodate delays. A session may last days.
Activity events + database changelog. Example: enrich user activity with profile information. Load a local copy of the database (from CDC changelog) into the stream processor. For each activity event, look up in the local hash table. Keep the local copy up to date by consuming the profile changelog stream.
This is basically a broadcast hash join (Chapter 10) that runs continuously, with the "small" input kept up to date via CDC. The join window is conceptually infinite for the table side; no window for the stream side.
Two database changelogs. Example: Twitter home timeline cache. Streams: tweets (send/delete) + follows (follow/unfollow). The join maintains a materialized view of "SELECT ... FROM tweets JOIN follows ON ... GROUP BY follower_id."
Mathematically: if u and v are tables, then
(u·v)' = u'·v + u·v' (the product rule from calculus). Changes to either input
produce changes to the join result.
If state changes over time, which version do you join with? Example: tax rates change over time; invoices need the rate at the time of sale, not the current rate. If event ordering across streams is undetermined, the join becomes nondeterministic — rerunning the same job on the same input may produce different results.
In data warehouses this is a slowly changing dimension (SCD). Solution: versioned identifiers — every time the tax rate changes, it gets a new ID; invoices reference the ID at the time of sale. This makes joins deterministic but prevents log compaction (all versions must be retained).
Batch processing's fault tolerance is easy: discard the output of failed tasks, retry. Stream processing can't do this because the stream never ends. Key techniques:
Break the stream into small batches (~1 second). Each batch is processed like a miniature MapReduce job. If a batch fails, retry it. Implicitly provides a tumbling window equal to the batch size (by processing time, not event time).
Periodically save operator state to durable storage (HDFS). Barriers in the message stream trigger checkpoints without forcing a fixed window size. On crash, restart from the latest checkpoint.
To prevent external side effects (database writes, downstream messages) from being applied twice on retry, the stream processing framework must atomically commit both the output and the consumer offset. Google Cloud Dataflow, VoltDB, and Kafka (KIP-98) implement this internally — unlike XA, they don't span heterogeneous systems.
Design operations so performing them twice has the same effect as once:
Requires: deterministic processing, same-order replay (log-based broker), and fencing (to prevent zombie nodes). Idempotence can achieve exactly-once semantics with low overhead.
Stream processors maintain state for windows, joins, and aggregations. Recovery options:
The key ideas of Chapter 11:
Select the correct answer for each question.
Scoring: 8/10 = Mastery. 6/10 = Review. <6/10 = Reread sections.
1. What fundamentally distinguishes stream processing from batch processing?
The input is unbounded — events arrive continuously and the job never finishes Stream processing handles events while batch processing handles files Only stream processors can scale to multiple machines2. How does a log-based message broker (Kafka) differ from a traditional JMS/AMQP broker regarding message delivery tracking?
It acknowledges each individual message to the broker after processing It records a consumer offset periodically instead of acknowledging each message It deletes messages immediately after delivery to minimize storage3. What problem with dual writes does change data capture solve?
It eliminates the need for a message broker entirely CDC makes the source DB the single leader and defines the authoritative order through the change log It provides synchronous read-your-writes consistency across all derived systems4. What is the key difference between change data capture and event sourcing?
CDC captures only inserts, while event sourcing captures all operations CDC captures low-level state changes; event sourcing captures domain-level user intent CDC deals with databases while event sourcing deals with message queues5. What does the relationship "state = ∫ event stream" mean?
State is ephemeral; events are the only durable storage The changelog is the truth; the database is a cache of latest values Both events and state are independent and equally authoritative6. Why is windowing by processing time problematic?
It cannot be implemented in any stream processing framework Backlog replay creates artificial spikes and events go to wrong windows It requires complex distributed clock synchronization7. What type of window would you use for website sessionization (grouping all clicks by a user within 30 minutes of inactivity)?
Tumbling window (fixed 30-minute intervals) Hopping window (5-min hop, 30-min length) Session window (variable duration, ends on inactivity gap)8. In a stream-table join (stream enrichment), how does the stream processor keep its local copy of the database up to date?
It queries the remote database for each activity event Subscribes to the CDC changelog and updates the local hash table incrementally Loads a snapshot of the database at startup9. How does Flink achieve fault tolerance without microbatching?
It saves operator state to disk every 10 seconds Barriers trigger consistent checkpoints; restart from last checkpoint on failure Idempotent writes to durable storage10. What is the time-dependence problem in stream joins and how is it solved in data warehouses?
It only affects nondeterministic joins SCD solves it by versioning: each version gets a unique ID; references use the version at time of sale It only affects joins within a single partitionBatch → bounded input, finite, sort-based, deterministic
Stream → unbounded, continuous, time-aware, stateful
Core problem: can't sort an infinite stream → no sort-merge joins
a) Direct → UDP multicast, ZeroMQ, webhooks. Problem: offline consumers lose messages
b) JMS/AMQP broker → centralized, message-level ack, destructive read, transient
c) Log-based (Kafka) → partitioned, offset-tracked, durable, replayable
Consumer patterns: load-balancing (1 msg → 1 consumer) v/s fan-out (1 msg → all consumers)
Topic → group of partitions
Partition → append-only log on disk, totally ordered via offsets
Consumer group → each partition assigned to one consumer (coarse parallelism)
Offset = consumer's position in partition (like DB LSN)
Replayability → reset offset, reread from any point
Circular buffer on disk → retention limit, not memory limit
App writes to DB + search index + cache independently
Race condition → different order at each system → permanent inconsistency
Partial failure → one succeeds, one fails → inconsistency
Solution → CDC: DB is leader, derived systems consume its change log
CDC → low-level state changes (row updates), app unaware, log compaction possible
Event sourcing → domain events (user intent), app built on events, full history retained
State = ∫ event stream ; change stream = d(state)/dt
Pat Helland: "log is truth, DB is cache of latest values"
CQRS → separate write-optimized log from read-optimized views
Event time → timestamp in event (correct, enables deterministic replay)
Processing time → wall clock when processor sees event (simple, breaks under lag)
Stragglers → events arriving after window "completed"
Solutions: ignore stragglers, publish corrections, watermarks
Device clock untrusted → log 3 timestamps (occurred, sent, received), estimate offset
a) Tumbling → fixed, no overlap (10:03-10:04)
b) Hopping → fixed, overlap for smoothing (5-min window, 1-min hop)
c) Sliding → all events within N of each other (continuous)
d) Session → variable duration, ends on inactivity gap
a) Stream-stream → windowed join, both activity streams (search + click)
b) Stream-table → enrichment, activity + CDC changelog (profile lookup)
c) Table-table → MV maintenance, two DB changelogs (tweets + follows → timeline)
All require state: buffers, hash tables, or local DB replicas
SCD → versioned identifiers to avoid nondeterminism from time-dependent joins
Batch → discard partial output of failed tasks, retry
Stream → can't discard all output (infinite)
a) Microbatching (Spark) → ~1 sec batches, retry failed batch
b) Checkpointing (Flink) → consistent snapshots via barriers, restart from checkpoint
c) Atomic commit → framework commits output + offset atomically (KIP-98, Cloud Dataflow)
d) Idempotence → include offset in writes, check before applying
State recovery → local + checkpoint, replicated change stream, or replay input
Primary source: Kleppmann, M. (2017).
Designing Data-Intensive Applications, Chapter 11: “Stream Processing.”
O'Reilly Media.
Recommended supplements: Jay Kreps' "The Log: What Every Software Engineer
Should Know" (the single best explanation of Kafka's design), the Dataflow Model paper (Akidau
et al., VLDB 2015), the Flink checkpointing paper (Carbone et al., arXiv 2015), the Spark
Streaming paper (Zaharia et al., HotCloud 2012), Pat Helland's "Immutability Changes
Everything" (CIDR 2015), and the MillWheel paper (Akidau et al., VLDB 2013).
Ask follow-up questions to drill into: how Kafka achieves 2M writes/sec on 3 machines, the exact mechanism of Flink's checkpointing barriers, how watermarks work in practice, the Lambda Architecture debate (why Jay Kreps says it's obsolete), how CDC handles schema changes, the subtleties of exactly-once vs effectively-once semantics, or how CQRS and event sourcing play out in production.