Skip to the chapter
←System DesignGGroundworkSDSystem Design
System Design/Advanced/A7
27 / 36 written chapters
/

The cover
  1. B1The mental model
  2. B2Client-server basics
  3. B3Vertical vs horizontal
  4. B4Databases in design
  5. B5Caching fundamentals
  6. B6APIs & communication
  7. B7Walkthrough: URL shortener
  1. I1Load balancing in depth
  2. I2CDNs
  3. I3Message queues & async
  4. I4Consistency models
  5. I5Replication
  6. I6Rate limiting & throttling
  7. I7Designing for availability
  8. I8Storage systems
  9. I9Search systems
  10. I10Microservices
  11. I11Security basics
  12. I12Walkthrough: feed/chat
  13. I13Walkthrough: notifications
  1. A1CAP theorem in depth
  2. A2Transactions and isolation
  3. A3Storage engines
  4. A4Sharding at scale
  5. A5Time, ordering and IDs
  6. A6Distributed consensus
  7. A7Batch & stream processing
  8. A8Fault tolerance
  9. A9Observability at scale
  10. A10Release safety
  11. A11Capacity estimation
  12. A12Case studies
  13. A13Walkthrough: key-value store
  14. A14Walkthrough: web crawler
  15. A15Walkthrough: payments
  16. A16Tradeoff thinking

SDA7 · Part 3 · Advanced

Batch & stream processing

The same computation can run over data that has ended or data that never will, and almost every hard question is about what time means and when you are allowed to call an answer final.

30 min read3 diagrams12 sections0 exercises
27 of 36

In this chapter

  • One computation, two kinds of input
  • From MapReduce to Spark: what actually changed
  • Log-based streaming: the log is the source of truth
  • Event time against processing time
  • Watermarks: a heuristic for "nothing older is coming"

and 7 more

about 30 min
12On this page
  1. One computation, two kinds of input
  2. From MapReduce to Spark: what actually changed
  3. Log-based streaming: the log is the source of truth
  4. Event time against processing time
  5. Watermarks: a heuristic for "nothing older is coming"
  6. Windows: three shapes of "per minute"
  7. State and checkpoints: how a stream job survives a crash
  8. Delivery guarantees, and what "exactly-once" means here
  9. Lambda against Kappa
  10. Change data capture and derived data
  11. Worked example: ad-click aggregation
  12. Recognizing it in an unseen problem

One computation, two kinds of input

Batch and stream processing look like two technologies, and the useful way to see them is as one idea applied to two kinds of input. Tyler Akidau's framing, which most of the field has adopted, splits data by finiteness, not by engine: a bounded data set has an end (yesterday's click log, a table snapshot) and an unbounded one does not (clicks as they happen). A batch job is what you run when the input is bounded; a stream job is what you run when it is not. Run a stream engine on a bounded input and you get a batch job. The reverse is where people go wrong: running a nightly job every minute is not streaming, it is a batch system with a short fuse.

 BatchStream
InputBounded: a fixed set of files or a snapshotUnbounded: records keep arriving, forever
When is the answer final?When the job finishesNever, so you decide when to emit and what to do about stragglers
Latency to a resultMinutes to hours (the job's schedule plus its runtime)Seconds or less
Failure recoveryRerun the failed tasks (or the whole job) over the same inputRestore the last state snapshot and rewind the input to match it
Reprocessing after a bugThe default operation: run the fixed code againReplay the retained log through a new job instance
Natural fitReports, model training, backfills, reconciliationAlerts, live dashboards, fraud signals, anything where a stale answer has lost its value

The practical rule: a bounded input lets the engine use perfect knowledge. It can sort by timestamp and wait for the end of the data, so it never has to guess when a window is complete. An unbounded input forces the guess, and the rest of this chapter is about making it well.

From MapReduce to Spark: what actually changed

MapReduce (Dean and Ghemawat, OSDI 2004) is a programming model with two user-supplied functions: map turns input key/value pairs into intermediate key/value pairs, and reduce merges all the values that share an intermediate key. In between, the runtime does the shuffle: it partitions the map output by key, moves it across the network and groups it, so that every value for one key lands at the same reducer. The programmer writes two functions; the runtime handles partitioning, scheduling, machine failure and communication. The paper reports that a typical computation processes many terabytes on thousands of machines, and that over a thousand jobs a day ran on Google's clusters.

The model's limit is the shape of real pipelines. One MapReduce job is one map-shuffle-reduce; a useful analysis is a chain of them, and in the classic Hadoop arrangement each job reads its input from a distributed file system and writes its output back to one. Iterative algorithms (graph ranking, machine learning) pay that disk round trip on every pass. Spark, from the Berkeley paper on Resilient Distributed Datasets (NSDI 2012), attacked exactly that: keep the working data in memory across steps, which the authors say can improve performance by an order of magnitude for iterative and interactive work. The trick is how it stays fault tolerant without replicating memory. An RDD is built by coarse-grained transformations (map, filter, join) applied to a base data set, and Spark records the chain of transformations. If a partition is lost, Spark recomputes it from the transformations that produced it, the lineage, instead of restoring a copy.

What did not change Spark still shuffles. Its documentation calls the shuffle "an expensive operation" because it involves disk I/O, serialization and network I/O; map output is kept in memory until it does not fit, then sorted and written to files that reducers read, and shuffle files are kept so a lineage recompute need not redo them. The shuffle is where batch jobs spend their time, and the practical advice (aggregate before you shuffle, so prefer reduceByKey over groupByKey) is the same idea as the combiner and as the local pre-aggregation you will use in the ad-click design below.

Log-based streaming: the log is the source of truth

Stream processing started as message-passing between operators. The model that won puts a replicated log in the middle (see the message queues chapter for queues against logs). Events are appended to a Kafka topic; a stream engine such as Flink or Kafka Streams reads it with a consumer position that is just an offset, computes, and writes results to another topic or a store. Three properties of the log carry the whole design:

  • Replay. The input is retained, so a job that crashes, or ships a bug, can be pointed back at an older offset. Reprocessing is a first-class operation, not a disaster procedure.
  • Many independent readers. The fraud job, the dashboard job and the warehouse loader each keep their own offsets and cannot slow one another.
  • A position that is one number per partition. A consumer's position in a partition is a single integer, the offset of the next message to read, which makes checkpointing it cheap. That is what lets a stream engine snapshot "how far I have read" together with "what I have computed".

The idea underneath is stream-table duality: a stream is the changelog of a table (replay it from the start and you rebuild the table), and a table is a snapshot of the latest value per key that can be turned back into a stream. Log compaction, covered in the queues chapter, is that duality implemented in storage. It matters later, because it is why a compacted topic can serve as the rebuild source for a derived view.

Event time against processing time

Every record has two clocks. Event time is when the thing happened on the device that produced it; processing time is when your system observes it. Flink's docs describe processing time as simplest and lowest latency but nondeterministic, because it depends on the speed at which records arrive; in event time, progress depends on the data, not on any wall clock. The gap between the two, the skew, is never zero and never constant: a phone in a tunnel delivers a click two minutes late, a Kafka consumer that fell behind delivers everything late, and a mobile client batches events and uploads them every few minutes.

If you aggregate by processing time, "clicks in the 12:00 minute" means "clicks my pipeline happened to see during 12:00", which changes when the pipeline is slow, restarts or replays. Billing, ranking and anything you will be audited on must use event time. The cost is that event-time answers are never obviously complete, which is the problem the next section solves.

processing time (arrival), seconds after 12:00:00, 0 to 190 event time (seconds after 12:00:00, 0 to 90) window end, 60 s e1 e2 e3 +12 s e4 e5 e6 +45 s e7 +150 s no skew
Schematic of the dry run below, scaled to the 12:00 minute. Distance below the dashed diagonal is skew. e1, e2, e4 and e5 are about a second behind; e3 is 12 s late, e6 is 45 s late and e7 is 150 s late. Every point under the red line belongs to the 12:00 window, however late it arrives.

Watermarks: a heuristic for "nothing older is coming"

A window over event time has to close some time. The engine cannot know that no late record is on its way, so it estimates. A watermark is a marker that flows through the stream with a timestamp t and the claim "event time has reached t; elements at or below t should no longer arrive". Flink generates them at or near the sources; a window fires once the watermark passes its end. The common strategy is bounded out-of-orderness: the watermark is the largest event timestamp seen so far, minus a fixed bound (Flink's own example emits currentMaxTimestamp - maxOutOfOrderness - 1 milliseconds). Spark Structured Streaming defines it the same way: the maximum event time seen, minus a threshold you set, such as 10 minutes.

Three consequences are worth saying out loud, because each is a trade-off:

  • The bound is a dial between latency and completeness. A 10-second bound delays every window result by about 10 seconds past the window end, and still loses anything later than that. A 10-minute bound loses far less and delays every result by 10 minutes. There is no correct value, only the lateness distribution of your data and what a wrong answer costs. Akidau calls watermarks "a reasonably accurate heuristic estimate", not a proof.
  • The minimum rules, so one slow input stalls everything. An operator with several inputs, or one after a keyBy, takes the minimum of its inputs' watermarks (Spark's default policy for multiple streams is also the minimum). An input that goes quiet, such as an idle Kafka partition, holds the minimum back and no window anywhere fires. Flink's answer is withIdleness, which marks an input idle after a timeout so it stops holding the watermark back.
  • Spark's guarantee is one-sided. Data less than the threshold behind the latest processed event time is always aggregated; data later than that may or may not be, and the more delayed, the less likely. Do not design as if late means dropped.

What happens to a late record is a policy, not an accident. Flink's default allowed lateness is zero: once the watermark is past the window end, the late element is dropped. With allowed lateness greater than zero, a late element that still falls inside the window's end plus that lateness is added and the window fires again with an updated result, so downstream must tolerate updates. Past that, you can route the element to a side output instead of discarding it silently, and let a batch job reconcile it later.

Windows: three shapes of "per minute"

tumbling, 1 minute: each event is in exactly one window 12:00 12:01 12:02 12:03 sliding, 10 minutes every 5: each event is in two windows [12:00, 12:10) [12:05, 12:15) [12:10, 12:20) session, gap 30 minutes: the data decides the edges session 1: 10:00 to 10:40 session 2: 10:50 to 11:20 clicks at 10:00, 10:10, 10:50
Tumbling windows partition time, sliding windows overlap it, and session windows ignore the clock and close after inactivity. Red dots are events. Each row has its own horizontal scale; in the session row a window runs from its first event to its last event plus the 30-minute gap.
WindowDefinitionOne event belongs toUse for
TumblingFixed length, no overlap (5-second windows in Flink's example)One windowPer-minute counts, hourly totals, billing periods
SlidingFixed length that starts every slide interval; overlaps when the slide is shorter than the lengthLength ÷ slide windows"Last 10 minutes, refreshed each minute" moving averages and rate alerts
SessionCloses after a gap with no events; merges windows closer than the gap; no fixed start or endOne session, and a late event can merge twoUser activity, visit length, burst detection
GlobalOne window per key; the default trigger never fires, so you supply a triggerOne windowCustom logic, such as emit every N events

Do the arithmetic before choosing. A one-hour window that slides every minute puts each event in 60 windows. At a steady 11,574 events a second that is 11,574 × 60 = 694,440 window assignments a second, and each key holds 60 open windows of state. Engines reduce the work by sharing partial results between windows, but the state and the emit rate still scale with length ÷ slide. A session example, gap 30 minutes: clicks at 10:00, 10:10 and 10:50 give two sessions. The first spans 10:00 to 10:40 (the last event plus the gap), and the 10:50 click is 40 minutes after 10:10, so it starts a second, spanning 10:50 to 11:20. If a late click stamped 10:30 then arrives, its own window [10:30, 11:00) overlaps both, and the sessions merge into one that spans 10:00 to 11:20. That merge is why session windows need a merging trigger and are the shape that late data disturbs most.

State and checkpoints: how a stream job survives a crash

A window count, a deduplication set, a join buffer: all are state, held in the operators, and a job that cannot recover it is useless for anything beyond a stateless filter. Flink's approach, from the Carbone et al. paper on asynchronous barrier snapshotting, is this. The checkpoint coordinator tells the sources to record their offsets and insert numbered barriers into their streams. A barrier flows with the data; when an operator sees barrier n it snapshots its state, and an operator with two inputs aligns them, so checkpoint n contains the effect of every event before barrier n and none after it. The result is a consistent cut: the source offsets and the operator state that correspond to exactly those offsets. Processing carries on while snapshots are written out asynchronously.

Recovery follows directly. Restore every operator's state from the last completed checkpoint, rewind each source to the checkpointed offset, and continue. Records between the checkpoint and the crash are processed again, but against state that has forgotten them, so state is not double counted. State size decides the backend: Flink's heap-based backend keeps state in JVM memory (fast, limited by memory, GC pressure), while the RocksDB backend keeps it on local disk so it can exceed memory and supports incremental snapshots, at a speed cost the docs put at roughly ten times slower than heap.

Dry run: crash and catch-up, with planning numbers

Assumptions, all planning figures: the job reads 11,574 events a second on average, checkpoints every 30 seconds, crashes 20 seconds after the last checkpoint, takes 60 seconds to restart, and can process at twice the arrival rate once it is running again.

StepWhat happensNumber
1Events processed after the last checkpoint and lost with the crash (their effect on state)20 s × 11,574 = 231,480
2Backlog waiting at the input when the job is running again (20 s to redo, plus 60 s of restart)(20 + 60) s × 11,574 = 925,920
3Capacity at 2× arrival23,148 events a second
4Net drain rate (capacity minus ongoing arrivals)23,148 − 11,574 = 11,574 a second
5Time to catch up925,920 ÷ 11,574 = 80 seconds
6Output delay seen by a read-committed consumer of an exactly-once Kafka sink, at worst, from checkpoint-driven commits aloneabout the 30 s interval plus the checkpoint's own duration

Two lessons. Catch-up time is (downtime + replay window) ÷ (capacity ÷ arrival − 1): with no headroom (capacity equal to arrival) a job never catches up, so size stream jobs with spare capacity. And frequent checkpoints shrink the replay window but cost I/O and, with an exactly-once Kafka sink, they are also how often output becomes visible.

Delivery guarantees, and what "exactly-once" means here

The queues chapter showed why exactly-once delivery is impossible and exactly-once effect is built from at-least-once plus idempotence or transactions; read that first, and here is how a stream engine applies it. Flink's documentation is blunt about the term: exactly-once does not mean each event is processed once, it means every event affects the Flink-managed state exactly once, because recovery replays source data. End to end, you need three things:

  • A replayable source (a Kafka offset you can rewind to).
  • State restored from a consistent checkpoint, so the replay does not double count.
  • A transactional or idempotent sink, so output written for records that will be replayed does not appear twice.

For Kafka output the engine uses Kafka's own machinery: the idempotent producer deduplicates retries with sequence numbers, and transactions let a producer write across partitions atomically (both are in the queues chapter). Flink's Kafka sink in EXACTLY_ONCE mode writes records inside a Kafka transaction that commits when the checkpoint completes, which has three operational consequences from the connector docs: downstream consumers must use isolation.level=read_committed or they will see records that may be aborted; records are not visible until a checkpoint completes, so checkpoint interval sets output latency; and the docs strongly recommend a producer transaction timeout larger than the maximum checkpoint duration plus the maximum restart time, because otherwise Kafka can expire an uncommitted transaction and data may be lost.

⚠ The guarantee stops at the edge of the transaction Everything above holds for Kafka-to-Kafka work. The moment a job calls an email provider, a payment API or a non-transactional database, a replayed record repeats the effect. For those sinks make the write idempotent (a deterministic key, an upsert) instead of trusting the engine. Spark's table of sinks says the same thing in its own words: the file sink is exactly-once, the Kafka sink and the foreach sink are at-least-once, and foreachBatch is listed as depending on the implementation, with the text saying it is at-least-once by default unless you use the batch id to deduplicate.

Lambda against Kappa

The Lambda architecture answers "streaming is fast but approximate, batch is slow but correct" by running both over the same immutable input and merging the results at query time. Jay Kreps' 2014 critique names the cost: the transformation logic must be implemented twice, in two complex distributed systems, and kept equal. His words are that he does not think the problem is fixable, and that abstraction layers compiling one program to both engines helped little. Akidau went further: well-designed streaming systems are a strict superset of batch, and Lambda belongs to the history of big data.

The alternative is the one Kreps proposed in the same post and half-jokingly suggested calling the Kappa Architecture ("it may be too simple of an idea to merit a Greek letter"): one pipeline and a retained log. To change the logic you start a second instance of the same job from the beginning of the log, writing to a new output table; when it catches up you switch readers to the new table, then stop the old job and delete the old table. The code is written once, and reprocessing is something you do every time the code changes.

 LambdaKappa
Code pathsTwo (batch and speed layer)One
How you correct historyThe batch layer recomputes it, and the merged view moves to the corrected dataReplay the log through the new job into a new table, then switch
What it needsA batch system plus a stream system, and a merge at read timeA log retained as long as your longest reprocessing window
Where it hurtsTwo implementations that disagree for subtle reasonsReplaying a long history is slow and needs spare capacity; the log's retention caps how far back you can go
Honest use todayRarely chosen fresh; found in systems that grew into itA common starting point for new log-centric designs (the author's judgement, not a measured fact); a separate batch reconciliation job is often kept where money is involved

Notice what the table does not say: batch is not obsolete. Batch is the right tool when the input is bounded and efficiency matters, and a nightly reconciliation that recounts the day exactly is how you catch what the stream dropped. The modern pattern is one logic, two execution modes (Flink's batch mode, Spark's unified APIs), not two programs.

Change data capture and derived data

Every system has a primary store, and then a growing set of derived data: the search index, the cache, the analytics warehouse, the recommendation features. The naive way to keep them current is for the application to write to each one (a dual write). It fails the way the queues chapter's outbox section shows: the writes are not atomic across systems, so a crash between them leaves the index and the database disagreeing, and two concurrent writers can apply their updates in opposite orders to the two stores, so they disagree permanently.

Change data capture fixes it by making the database's own change log the single source. Log-based tools such as Debezium read the database's change stream (the MySQL binlog, the PostgreSQL logical replication stream) and publish the events, generally to one Kafka topic per table. Every derived store then consumes that topic and applies the changes in order, so each is a deterministic function of one log. Three design points follow:

  • Expect duplicates. Debezium's FAQ states that after a crash consumers may see events more than once. Apply changes idempotently: upsert by primary key, and carry the source position so a stale replay cannot overwrite newer data.
  • Bootstrap, then tail. A new derived store needs the existing rows, not just future changes. Either snapshot the table and then switch to the log at the matching position, or, for a table that is read as a changelog, replay a compacted topic, which keeps the latest record per key.
  • Deletes are events. A delete must reach every derived store; in a compacted topic that is a record with a key and a null value (a tombstone), and a consumer that stays away longer than the delete-retention period can miss it.

The payoff is the reprocessing story again: to add a new index, or to fix a bug in how one is built, you replay the log into a fresh instance and cut over. That is why teams that adopt a log tend to adopt CDC, and why the outbox from the queues chapter and CDC are the same idea at two scales: a transactionally written change record, then a relay that is allowed to repeat itself.

Worked example: ad-click aggregation

The prompt (it is a chapter of Alex Xu's System Design Interview, Volume 2; the framing here is mine): advertisers want to know how many times each ad was clicked, per minute, within seconds, and they will be billed from the numbers. That sentence sets the constraints. The counts must be correct (money), nearly real-time (advertisers watch campaigns), tolerant of late and duplicate events (phones, retries), and recoverable (a crashed job cannot lose or double count a click). The figures below are this chapter's assumptions, not quotations from any source, so change them if the interviewer does.

Capacity estimation

  • Volume: 1 billion clicks a day ÷ 86,400 s = 11,574 clicks a second on average. Allow a 5× peak: about 57,870 a second.
  • Raw storage: 100 bytes per click event × 1 billion = 100 GB a day, or 36.5 TB a year before replication. Keep it: raw events are what you replay and reconcile from.
  • Ingest bandwidth at peak: 57,870 × 100 B = 5.8 MB/s. Under the 10 MB/s per-partition planning figure from the queues chapter, so volume alone does not force many partitions; parallelism for the aggregation does.
  • Aggregated output: assume 50,000 distinct ads are clicked in an average minute. One row per ad per minute is 50,000 × 1,440 = 72 million rows a day; at 40 bytes a row that is 2.88 GB a day, about 35 times smaller than the raw data. (At 694,444 clicks a minute against 50,000 rows, each row summarises about 14 clicks.)
  • Partitions: if one aggregation task handles about 10,000 events a second, peak needs 57,870 ÷ 10,000 ≈ 6 tasks. Choose 12 partitions, which gives about 2× headroom and room to grow, and partition the topic by click id, not by ad id (the reason is in the hot-key bullet below).
click service click id Kafka 12 partitions stream job pre-aggregate, merge by ad, 1 min aggregates store upsert (ad, minute) raw archive object storage nightly batch reconciliation dashboard, billing export late events: side output The stream path answers in seconds. The batch path answers exactly. Both read the same raw events, so billing uses the corrected numbers.
One log, one job, and a batch path that exists only to settle the books. Events that miss the lateness window go to a side output that lands in the archive with the raw data, and the nightly batch job recounts the archive and overwrites the store's rows.

The decisions, in the order an interviewer pushes

  • Where does deduplication happen? Clients retry and some are malicious. Stamp each click with a unique click id at the click service and partition the topic by it, so every duplicate of a click lands in the same partition and one task can keep a deduplication set in its own state (bounded by the lateness window plus a margin, or it grows without limit). Combine with the exactly-once chain: replayable source, checkpointed state, idempotent sink.
  • What is the hot-key answer? A viral ad can take, say, 20% of all clicks: 0.2 × 57,870 = 11,574 clicks a second, more than the 10,000 a second one task handles. If the topic were partitioned by ad id, all of it would land in one partition, and no downstream trick helps a task that must still read every event. Because the topic is partitioned by click id, the hot ad's clicks arrive spread across all 12 partitions. Then use two-stage aggregation: each task pre-aggregates its own slice per (ad, minute), and a second stage, keyed by ad id, merges the partial counts. That is the combiner idea from MapReduce, and counts are additive, so partials merge exactly. The second stage now receives at most 12 partials per ad per minute instead of every click. When a late event updates a task's partial, that task re-emits its cumulative partial and the second stage replaces its previous value from that task before summing.
  • Why event time, and how much lateness? Billing must reflect when the click happened. Pick the watermark bound from the measured lateness distribution (the 99th percentile of skew, say), keep a longer allowed lateness for updates, and send what is left to the side output.
  • Why also a batch path? Because the stream's answer is an estimate of "done" by construction. The batch job recounts from the raw archive for finished days with perfect knowledge of arrival (a bounded input needs no watermarks), and its counts replace the stream's. This is lambda's good idea, kept small: a correction, not a second pipeline of logic.
  • Why does the sink upsert? The window result can update (allowed lateness) and can replay (recovery). Writing (ad_id, minute) → count as an overwrite makes both harmless; an increment would double count.

Dry run: one window, seven events, a 10 s bound and 60 s of allowed lateness

Ad A, tumbling windows of one minute, watermark = latest event time seen minus 10 s (the one-millisecond offset in Flink's example is ignored here), allowed lateness 60 s. Window W is [12:00:00, 12:01:00). The cleanup time for W is its end plus the lateness, 12:02:00.

EventEvent timeArrivesLatest event time seenWatermark after itWhat happens
e112:00:0512:00:0612:00:0511:59:55Added to W (count 1)
e212:00:5812:00:5912:00:5812:00:48Added to W (count 2)
e312:00:5212:01:0412:00:5812:00:4812 s late, out of order, but above the watermark: added to W (count 3)
e412:01:0312:01:0412:01:0312:00:53Belongs to the next window, W2 [12:01, 12:02) (count 1). W has not fired: 12:00:53 is before 12:01:00
e512:01:2012:01:2112:01:2012:01:10W2 count 2. The watermark passes 12:01:00, so W fires with 3
e612:00:4012:01:2512:01:2012:01:10Behind the watermark (12:00:40 < 12:01:10), but W is kept until 12:02:00: added, W fires again with 4
e712:00:3012:03:0012:03:00 (other events moved it)12:02:50The watermark is past 12:02:00, so W is gone: e7 goes to the side output, not W

Count the answers. The stream's final value for W is 4; the true count from event time is 5 (e1, e2, e3, e6, e7). The nightly batch recount reads the archive, finds all five and overwrites the row with 5, which is what billing sees. Skew values (arrival minus event time): e3 is 12 s, e6 is 45 s, e7 is 150 s. With a 10 s bound and no allowed lateness, e6 and e7 would both have been dropped, and W would have fired once with 3; allowed lateness recovers e6, and only e7, at 150 s of skew, needs the side output and the batch. With a 150 s bound every result would have waited two and a half minutes. The bound, the allowed lateness and the reconciliation are three settings on one trade-off.

Say it like this → "Billing needs correct counts and advertisers want them in seconds, so I'll use both paths and be honest about what each guarantees. Clicks go into Kafka with a unique id, partitioned by click id so duplicates meet and a viral ad is spread over every partition. A Flink job aggregates one-minute event-time windows with a watermark from the measured lateness, pre-aggregates in each task and then merges by ad id, and upserts (ad, minute) to the store, so replays and late updates are harmless. Checkpoints plus a replayable source give exactly-once state, and the upsert gives an exactly-once effect at the sink. Whatever the watermark drops goes to a side output and the raw archive, and a nightly batch recount corrects the day. The stream gives speed; the batch gives the books."

Recognizing it in an unseen problem

  • Signals: "per minute", "real time", "top K in the last hour", "fraud detection", "metrics", "click counts", "keep the search index in sync", "rebuild a view", or any requirement that mixes a latency target with a correctness target.
  • First question: is the input bounded? A bounded job wants batch; an unbounded one wants a stream, and then you must say what time you use and when a result is final.
  • If counts feed money or compliance: event time, deduplication, an idempotent sink and a batch reconciliation. If a few lost samples are harmless (telemetry), processing time with at-most-once is fine and far cheaper.
  • If two stores must agree: do not dual write. Capture the change log, or use an outbox, and make each consumer idempotent.
  • If the logic will change: keep the log long enough to replay, and be able to start a new job instance and cut over, not run two codebases.
  • The pitfalls to name unprompted: the stalled watermark from an idle partition, the hot key, state that grows without bound (windows or dedupe sets that are never cleaned), and exactly-once claims that stop at the first external API call.
  • The follow-up you will get: "what about events that arrive an hour late?" The answer is a policy, not a wish: bound, allowed lateness, side output, reconciliation, and a stated tolerance.
Before you move on
  • Explain the difference between event time and processing time, and say which one a billing aggregate must use and why.
  • Trace the dry run: say which events land in which window, when the first window fires and with what count, and why the stream ends at 4 while the truth is 5.
  • Explain a watermark as a heuristic, and name the two ways it goes wrong: too tight drops data, too loose adds delay, and an idle input stalls it.
  • Choose a window shape for three requirements (per-minute totals, a ten-minute moving rate, visit length) and compute the state cost of a one-hour window sliding every minute.
  • Say what exactly-once means in a stream engine, which three ingredients it needs end to end, and where it stops being true.
  • Argue lambda against kappa, including what each does to the amount of code you maintain and what retention each requires.
  • Explain why change data capture beats dual writes for keeping a search index in sync with a database.
Sources and further reading
  • Dean and Ghemawat, MapReduce: Simplified Data Processing on Large Clusters (OSDI 2004) — map, reduce, the runtime's job, scale reported at Google
  • Zaharia et al., Resilient Distributed Datasets (NSDI 2012) — in-memory cluster computing with fault tolerance from coarse-grained transformations
  • Apache Spark RDD programming guide — recomputing lost partitions, the cost of the shuffle, shuffle files, reduceByKey over groupByKey
  • Akidau, The world beyond batch: Streaming 101 — bounded and unbounded data, event time against processing time, window types, watermarks as heuristics
  • Kreps, Questioning the Lambda Architecture — the cost of two code paths and the replay-based alternative
  • Apache Flink: timely stream processing — event time, watermarks, the minimum across inputs
  • Apache Flink: generating watermarks — bounded out-of-orderness and idle inputs
  • Apache Flink: windows — tumbling, sliding, session and global windows, allowed lateness, side outputs
  • Apache Flink: fault tolerance via state snapshots — barriers, alignment, what exactly-once covers, state backends
  • Apache Flink: Kafka connector — sink guarantees, transactions and checkpoint-driven visibility
  • Apache Spark Structured Streaming: event time and watermarking — the watermark formula, one-sided guarantee, sink guarantees
  • Debezium FAQ — log-based change capture and at-least-once delivery after a crash

Claims in this chapter were checked against these sources in October 2026. Figures marked "planning figure" or "assumption" are this chapter's own, not quotations from a source, and the ad-click numbers are illustrative.

✓Practise

5 questions drawn from 8, scored out of 5. This is practice: it runs in your browser, saves nothing and does not change what you have marked read.

A7

Finished reading?

0 of 36 chapters read in this series

→Next stop

  1. A6Previous · A6Distributed consensus (surface)
  2. A7you are here
  3. Next · A8Designing for fault toleranceAt scale something is always broken — the design question is never "will it fail" but "what happens to everything else when it does".23 min read

01On this page

12 sections · 0%

  1. 01One computation, two kinds of input
  2. 02From MapReduce to Spark: what actually changed
  3. 03Log-based streaming: the log is the source of truth
  4. 04Event time against processing time
  5. 05Watermarks: a heuristic for "nothing older is coming"
  6. 06Windows: three shapes of "per minute"
  7. 07State and checkpoints: how a stream job survives a crash
  8. 08Delivery guarantees, and what "exactly-once" means here
  9. 09Lambda against Kappa
  10. 10Change data capture and derived data
  11. 11Worked example: ad-click aggregation
  12. 12Recognizing it in an unseen problem

[] chapters · t top