Stream processing
Producer → broker → consumer
Messages arrive randomly at the producer rate; the consumer takes an exponential time per message. Try: with the producer faster than the consumer, compare the three policies. Buffering hides the problem until latency explodes; backpressure caps the producer at the consumer's pace.
Two kinds of broker: delete on ack, or keep a log
stream-load-balancingack-redeliverylog-based-brokerconsumer-offsetOne queue shared by two consumers
Each consumer prefetches up to 3 messages and acks one after 6 ticks of work. Try: in load-balancing mode, crash consumer A. Its unacked messages are redelivered to B, which has already processed later ones, so they appear out of order (red).
Keeping systems in sync: dual writes vs change data capture
dual-writeschange-data-capturelog-compactionTwo clients set the same key concurrently: X = A and X = B
Lines are network messages; their length is a random delay. Try: in dual-write mode press "Find a bad interleaving": the two writes reach the database and the index in opposite orders (Figure 11-4). Then switch to CDC: the index applies the database's change log in commit order, so it can lag but never disagrees.
Change log for a users table (key = primary key)
Faded entries are overwritten by a later write to the same key (or deleted by a tombstone). Try: append a few rounds, then compact. The log shrinks to one entry per live key, and replaying it still rebuilds exactly the current table, so a new derived system (say, a fresh search index) can bootstrap from the log alone.
Event log (append-only, immutable)
- 0ItemAdded(apples)
- 1ItemAdded(book)
- 2ItemAdded(apples)
- 3ItemRemoved(book)
- 4ItemAdded(socks)
- 5CheckedOut(3 items)
- 6ItemAdded(book)
Derived state = replay(events)
Try: remove something that is not in the cart (rejected command, no event). Drag the replay slider back to see the cart at any past moment; take a snapshot and watch how many events a replay still needs.
When it happened (x) vs when the processor saw it (y)
Green: on time. Amber: straggler that still made its window. Red: arrived after its window was closed (late). The dashed diagonal is zero delay; the red band is the outage, after which the backlog is processed in a burst.
Events per minute: windowed by processing time vs by event time
Try: play with the outage on. The processing-time chart shows a dip and then a fake spike at 4:00, though the real rate never changed. Event-time windows stay flat, but stragglers past the watermark are lost unless you raise the allowed lateness (and wait longer for every result) or publish corrections.
Four kinds of window on one timeline
stream-windowstumbling windows over one user's events
Fixed length, back to back: every event belongs to exactly one window.
Numbers are event counts per window. Try: click an event to see every window it lands in (one for tumbling, several for hopping); in session mode, shrink the inactivity gap until one session splits in two.
Search events ⋈ click events on session id, within a window
The processor indexes recent searches by session (blue bar = how long it keeps waiting). Green: click found in the window. Red: the click came too late and the search was already emitted as "not clicked". Try: shrink the window and watch measured click-through rate drop; grow it and the state the processor must hold grows too.
Input log → stream processor → external database
Write the output, then checkpoint the offset every few records (a microbatch). A crash replays everything since the last checkpoint: at-least-once.
Each record takes two steps (write output, record progress) in the order the mode dictates. Red stacks are duplicate side effects; dashed red boxes are records skipped forever. Try: crash mid-batch in each mode; then raise random crashes and compare totals.
- takeawayA log-based broker treats messages like a database treats its replication log: durable, ordered per partition, readable by many consumers at their own pace, and replayable. Consumer offsets make acks cheap and reprocessing easy.
- takeawayDon't write to several systems from the application. Make one system the leader and derive the rest from its change stream (CDC or event sourcing). Every view then sees the same writes in the same order, and new views can be built by replaying.
- takeawayWindow by event time, not processing time, and decide up front what to do with stragglers. Make side effects idempotent or commit them atomically with offsets, so crash-and-replay gives exactly-once results.
Key concepts · 20
book chapter 11- Event (in a stream)
A small, self-contained, immutable record that something happened at some point in time.
Events are generated once by a producer and processed by many consumers. Thinking of data as a sequence of events, rather than as mutable state, is the foundation for everything else in the chapter.
↑ see it in the visualization - Messaging system / message broker
A server that producers send events to and consumers receive them from, decoupling the two.
Brokers tolerate clients coming and going and absorb bursts. Two key design questions: what happens when producers are faster than consumers, and what happens if a node crashes.
RabbitMQActiveMQKafkaGoogle Cloud Pub/Sub↑ see it in the visualization - Backpressure (flow control)
Blocking the producer when the consumer cannot keep up, instead of dropping messages or buffering without bound.
The three options for an overloaded consumer are drop, buffer, or block. Buffering only postpones the problem and raises latency; dropping loses data; backpressure slows the whole pipeline to the speed of its slowest part.
TCP flow controlUnix pipes↑ see it in the visualization - Load balancing vs fan-out
Load balancing delivers each message to one of several consumers; fan-out delivers every message to all of them.
Load balancing parallelizes expensive processing; fan-out lets independent consumers each see the whole stream. Consumer groups in log-based brokers combine both.
↑ see it in the visualization - Acknowledgments and redelivery
A consumer acknowledges each message after processing; unacknowledged messages are redelivered if the consumer dies.
Redelivery prevents loss, but combined with load balancing it reorders messages: a redelivered message can reach another consumer after later ones were already processed.
AMQPJMS↑ see it in the visualization - Log-based message broker
A broker that stores messages in an append-only, partitioned log on disk; consumers read sequentially and messages are not deleted on consumption.
It gives durable, replayable, totally ordered (per partition) streams with high throughput. Parallelism is at the granularity of partitions, and a slow message holds up its whole partition.
Apache KafkaAmazon KinesisApache PulsarTwitter DistributedLog↑ see it in the visualization - Consumer offsets and lag
Each consumer group records how far it has read in each partition as a single offset number.
Offsets make acknowledgment cheap and replay trivial: rewind the offset to reprocess. A consumer that lags further behind than the retention window loses the messages that were deleted.
↑ see it in the visualization - Dual writes
Application code writing the same change to several systems (database, cache, search index) itself.
Two concurrent clients can have their writes arrive in different orders at each system, leaving them permanently inconsistent, and one write can fail while the other succeeds.
↑ see it in the visualization - Change data capture (CDC)
Observing every write to a database and exposing it as a stream that other systems can apply in the same order.
It makes the database the leader and derived systems followers of its replication log, so they converge on the same state. Initial state comes from a snapshot or a compacted log.
DebeziumMaxwellLinkedIn DatabusPostgres logical decodingMongoDB oplog↑ see it in the visualization - Log compaction
Periodically discarding older log entries for a key, keeping only the most recent value (and dropping keys with a tombstone).
The log then stays proportional to the size of the database rather than to its write history, yet a new consumer can still rebuild the full current state by reading it from offset 0.
Kafka compacted topics↑ see it in the visualization - Event sourcing
Storing every change to application state as an immutable event in an append-only log, and deriving current state by replaying it.
Events capture intent at the application level, history is never lost, and new read-optimized views can be derived from the same events. Snapshots keep replay fast.
Event StoreAxon↑ see it in the visualization - Commands vs events
A command is a request that may still be rejected; once validated it becomes an event, an immutable fact.
Validation must happen synchronously before the event is written. Consumers of the event cannot reject it, only compensate with further events.
↑ see it in the visualization - Event time vs processing time
Event time is when the event occurred (by the device's clock); processing time is when the stream processor sees it.
Queueing, network faults, and restarts delay events, sometimes by minutes. Windowing by processing time produces artifacts such as spikes after a backlog, so rates should be computed by event time.
↑ see it in the visualization - Stragglers and watermarks
A straggler is an event that arrives after its window was believed complete; a watermark is the processor's declaration that no earlier events are expected.
You can never be sure all events for a window have arrived. Either ignore stragglers (and monitor how many you drop) or publish a correction later.
↑ see it in the visualization - Window types
Tumbling (fixed, non-overlapping), hopping (fixed, overlapping), sliding (all events within an interval of each other), session (bounded by inactivity gaps).
Aggregations over an unbounded stream need a bounded scope. The window type determines which events are counted together and whether one event can count in several windows.
↑ see it in the visualization - Stream–stream join (window join)
Joining events from two streams that share a key and occur within some window of each other, e.g. searches and clicks.
The processor must keep state, an index of recent events from both sides, and the window decides when it stops waiting and emits a non-match.
↑ see it in the visualization - Stream–table join (enrichment)
Augmenting each event with data from a table, using a local copy of the table kept current by a changelog stream.
Avoids a remote database query per event. The join is time-dependent: an event joined before or after a profile update gets different data.
↑ see it in the visualization - Table–table join (materialized view)
Two changelog streams joined to maintain a materialized view of the join, such as a per-user timeline cache of tweets from followed accounts.
Instead of computing the join at read time, every tweet, delete, follow and unfollow incrementally updates the cached result.
Twitter home timelines↑ see it in the visualization - Microbatching and checkpointing
Breaking a stream into small blocks processed like mini batch jobs, or periodically checkpointing operator state and input offsets.
On failure the processor restarts from the last checkpoint and discards partial output, but any side effects already sent to external systems would be repeated.
Spark StreamingApache Flink↑ see it in the visualization - Idempotence and exactly-once
An idempotent operation has the same effect whether it is applied once or several times; with it, at-least-once processing yields effectively exactly-once results.
Storing the input offset alongside the output, or committing output and offset atomically, makes reprocessing after a crash harmless.
Kafka transactionsGoogle Cloud DataflowVoltDB↑ see it in the visualization