Skip to content
academia.sh

Lesson 14 / 14

Batch and Stream Processing Pipeline

The closing case where two processing models are used together in the same system: how the watermark cuts off late-arriving data, how the stream path's first published result diverges from the batch path's recomputation, the divergence's closing curve, and the overhead cost of running both paths at once.

Contents

Each of the previous four cases did its work either at write time or at query time; none needed both together in the same system. This case takes on that problem. The system counts an event stream in one-minute windows, and two paths are built: stream processing publishes a result within seconds, and batch processing recomputes the same window hours later from the log. The two produce different results from the same input, because the stream path must close a window and cannot see anything after that moment.

The decision to close is made by the watermark: a bound expressed in event time that says how far the stream has advanced along its time axis, and a window closes once this bound passes it. Despite the similar name, it is distinct from the Application Layer and Service Interaction course’s version stamp and the Resilience and Reliability course’s deployment stamp: one marks a record’s version, the other a deployment unit’s identity, this one marks progress in time. A record that arrives after the watermark has passed is late-arriving data, and it does not enter the published count.

Constraints

Functional requirement: accept an event, count it in 60-second windows by event time, publish the result, and correct it with late-arriving data.

Non-functional requirement, as a number: the first result is published at most 60 seconds after the window closes; its divergence does not exceed 30 per mille; the divergence drops below 0.5 per mille within 12 hours; the two paths’ combined overhead does not exceed 3 times the raw input.

Scope reduction: cross-stream joins, session windows, exactly-once processing guarantees, and a query language are not designed.

Assumptions

Code Assumption Value Rationale
TA1 daily events 900,000,000 total produced by the edges
TA2 event record 180 bytes ID, event time, type, field
TA3 window size 60 s the finest grain of reporting
TA4 delay distribution 0.94 uniform 0-5 s; 0.055 exponential 90 s; 0.005 exponential 4 hours most edges are fast, very few are offline
TA5 peak factor 3 ratio of the peak hour to the daily average
TA6 batch run interval 6 hours four runs a day
TA7 log retention period 30 days history the recompute can reach

TA4 is the load-bearing assumption: the entire difference between the two paths comes from the delay distribution’s tail. Without a tail, the two paths give the same result and there is no need for a second path.

Scale

// pipeline-scale.mjs — the scale calculation derived from the TA table and the cost of running both paths at once
const TA = { dailyEvents: 900_000_000, eventBytes: 180, windowSec: 60, peakFactor: 3, batchIntervalHours: 6,
  retentionDays: 30 };
const DAY = 86_400;
const eventsPerSec = TA.dailyEvents / DAY;
for (const [name, d] of [
  ["avg events/s", eventsPerSec],
  ["peak events/s", eventsPerSec * TA.peakFactor],
  ["daily log GB", (TA.dailyEvents * TA.eventBytes) / 1e9],
  ["log TB (retention)", (TA.dailyEvents * TA.eventBytes * TA.retentionDays) / 1e12],
]) console.log(name.padEnd(22) + d.toFixed(2).padStart(14));

console.log(`\nbatch runs every ${TA.batchIntervalHours} hours; the scan window determines the workload:`);
console.log("scan hours".padEnd(13) + "daily runs".padStart(14) + "batch multiple".padStart(15) +
  "combined total".padStart(15));
for (const W of [6, 12, 24]) {
  const runs = 24 / TA.batchIntervalHours;
  const mult = (runs * W) / 24;
  console.log(String(W).padEnd(13) + String(runs).padStart(14) + mult.toFixed(2).padStart(15) +
    (1 + mult).toFixed(2).padStart(15));
}
avg events/s                10416.67
peak events/s               31250.00
daily log GB                  162.00
log TB (retention)              4.86

batch runs every 6 hours; the scan window determines the workload:
scan hours       daily runs batch multiple combined total
6                         4           1.00           2.00
12                        4           2.00           3.00
24                        4           4.00           5.00

These numbers are in the calculation class. The lower table binds the last constraint: because the batch run executes every six hours, the same event is read more times as the scan window grows — once for a six-hour window, twice for twelve hours, four times for twenty-four hours. The stream path’s single pass brings the combined total to 2.00 / 3.00 / 5.00 times, and the 3-times constraint eliminates the twenty-four-hour scan outright.

The Difference Between the Two Paths

The watermark delay determines both publish latency and divergence, and the two move in opposite directions. The model below builds no cluster; it draws delay from TA4’s mixture and compares what the stream path counts against the batch path’s exact count.

// two-paths.mjs — in-process model: the difference in the result the stream path and the
// batch path produce over the same event set. No network, no cluster; the delay distribution is
// TA4's three-part mixture, the generator is hand-written, and the seed is visible. IT IS A MODEL.
const WINDOW = 60, WINDOWS = 120, EVENTS = 2000, SEED = 20260803;
const MIX = [[0.94, "uniform", 5], [0.055, "exponential", 90], [0.005, "exponential", 14_400]];  // TA4

let state = SEED;
const random = () => { state = (state * 1103515245 + 12345) % 2147483648; return state / 2147483648; };
function delay() {
  let u = random(), i = 0;
  while (i < MIX.length - 1 && u > MIX[i][0]) { u -= MIX[i][0]; i += 1; }
  const [, kind, p] = MIX[i];
  return kind === "uniform" ? random() * p : -Math.log(1 - random()) * p;
}
const S = (x) => MIX.reduce((a, [w, kind, p]) =>          // independent of the run: tail probability
  a + w * (kind === "uniform" ? Math.max(0, 1 - x / p) : Math.exp(-x / p)), 0);
const expectedLate = (L) => {                             // via a uniform position within the window
  let t = 0;
  for (let i = 0; i < 6000; i += 1) t += S(WINDOW + L - (i + 0.5) * (WINDOW / 6000));
  return t / 6000;
};

const total = WINDOWS * EVENTS;                           // arrival instant measured from the window's start
const arrivals = Array.from({ length: total }, () => random() * WINDOW + delay());

console.log(`model: ${WINDOWS} windows x ${EVENTS} events = ${total}, window ${WINDOW} s, seed ${SEED}`);
console.log(`delay mixture: ${MIX.map(([w, t, p]) => `${w}/${t}/${p}s`).join(" ")}\n`);
console.log("watermark delay".padEnd(16) + "publish delay".padStart(16) +
  "late per mille".padStart(16) + "expected per mille".padStart(20));
for (const L of [0, 15, 60, 120, 300]) {
  const late = arrivals.filter((o) => o > WINDOW + L).length;
  console.log(`${L} s`.padEnd(16) + `${WINDOW + L} s`.padStart(16) +
    ((late / total) * 1000).toFixed(2).padStart(16) + (expectedLate(L) * 1000).toFixed(2).padStart(20));
}

console.log(`\nthe divergence's closing (against the result published with a 60 s watermark delay):`);
console.log("after window closes".padEnd(21) + "still missing".padStart(17) + "divergence per mille".padStart(22));
for (const [name, T] of [["1 hour later", 3600], ["6 hours later", 21_600], ["12 hours later", 43_200],
  ["24 hours later", 86_400]]) {
  const remaining = arrivals.filter((o) => o > WINDOW + T).length;
  console.log(name.padEnd(21) + String(remaining).padStart(17) + ((remaining / total) * 1000).toFixed(3).padStart(22));
}
model: 120 windows x 2000 events = 240000, window 60 s, seed 20260803
delay mixture: 0.94/uniform/5s 0.055/exponential/90s 0.005/exponential/14400s

watermark delay    publish delay  late per mille  expected per mille
0 s                         60 s           82.46               84.30
15 s                        75 s           37.42               38.96
60 s                       120 s           24.67               25.58
120 s                      180 s           13.32               15.53
300 s                      360 s            5.37                6.32

the divergence's closing (against the result published with a 60 s watermark delay):
after window closes      still missing  divergence per mille
1 hour later                       711                 2.962
6 hours later                      251                 1.046
12 hours later                      69                 0.287
24 hours later                       0                 0.000

The measured per-mille figure is a measurement tied to this run; the expected per-mille figure is a calculation independent of the run, derived from the mixture’s tail probability. On all five rows, the two sit at the same order of magnitude.

The upper table binds both constraints at once. At a watermark delay of 0, the divergence is 82.46 per mille; at 15 seconds, 37.42 — both break the 30-per-mille constraint; at 60 seconds, 24.67 per mille satisfies it. On the other hand, publish delay is window size plus the watermark, and the “at most 60 seconds later” constraint bounds the watermark to 60 seconds. The two constraints intersect at a single value: watermark delay of 60 seconds.

The first row also corrects a misconception. At a watermark of zero, most of what is missed is not late-arriving data — it is an event that lands in the window’s final seconds and arrives at normal speed; even some events with a delay under five seconds cross the boundary. At 15 seconds, this share disappears entirely, and the drop from 82.46 to 37.42 per mille comes from there. What is left of the divergence really does come from the tail.

The lower table gives the closing curve: 2.962 per mille an hour later, 1.046 six hours later, 0.287 twelve hours later. The “drops below 0.5 per mille within 12 hours” constraint is not satisfied by a six-hour scan, but it is by a twelve-hour one. The workload constraint had already eliminated twenty-four hours; the closing constraint eliminates six hours. What remains is a twelve-hour scan window, and the total work comes out to exactly 3.00 times the raw input.

Design

The single source of truth is the event log, and both paths are its projection (the Scaling the Data Layer course’s Read–Write Separation topic, Event-Sourced Design). The write model is the log, the read model is the window counts; separating the two is the same topic’s Command and Query Separation. The table the batch path produces is a materialized view (the Data Distribution topic), and its refresh is not incremental — it is a recomputation of the twelve-hour window.

Overwriting the published value must be idempotent (the Resilience and Reliability course’s Distributed Correctness topic); the uniqueness key is the pair of window and path, so the result does not change even if the same run executes twice. The intake side sits on a message broker and competing consumers (the Application Layer and Service Interaction course’s Queues and Workflows topic); the peak of 31,250 events/s is this layer’s input, and when a consumer falls behind, backpressure (the Fault Isolation topic) propagates all the way to the edge. What the reader sees is eventually consistent (the Introduction to System Design course’s Fundamental Properties topic), and the staleness window here is a measured number: 0.287 per mille.

Deliberately unused pattern: edge caching. The edge caching from the Traffic Layer course’s Entry Points topic is not placed in front of the published counts, because the same window’s value keeps being corrected for twelve hours; opening a second staleness window at the edge would make the design’s one measurable guarantee — the closing time — unmeasurable. The second is the compensating transaction (the Distributed Correctness topic): what the batch path does is not a rollback but an overwrite, because the log does not change.

Eliminated Alternatives

Stream only is the cheapest: one times the work, publish in 120 seconds. But the 24.67-per-mille divergence never closes; there is no second pass to correct it.

Batch only is flawless on accuracy and its workload is one times. A window’s result is published, in the worst case, six hours later: 360 times the publish constraint.

Widening the watermark brings the divergence down to 5.37 per mille at 300 seconds and satisfies the 30-per-mille constraint with a single path; publish delay rises to 360 seconds, six times over the first constraint. Which constraint changes and the alternative wins: if the publish constraint is relaxed to 6 minutes, a single stream path suffices, and the second path’s 2.00-times extra overhead drops.

Failure Behavior and What Is Given Up

When the stream path stops entirely, the system keeps responding: the batch path produces the correct result once every six hours, publish delay rises from 120 seconds to six hours, and the divergence drops to zero. This is an unusual form of graceful degradation (the Fault Isolation topic) — the dimension that degrades is not accuracy but freshness. The reverse happens when the batch path stops: publish stays at 120 seconds, but the 24.67-per-mille divergence freezes. Neither failure loses data, because both paths derive from the log, which is kept for thirty days.

What is given up: the same event is processed twice. Total work is 3.00 times the raw input, and the same count is kept in two separate code paths; that the two agree is guaranteed not by sharing code but by a measurement. The first number given to the reader is also never final.

Summary

  • The watermark is the decision to close a window; because its delay moves publish latency and divergence in opposite directions, it is squeezed between two constraints.
  • At a watermark of 0 / 15 / 60 / 120 / 300 seconds, the divergence is 82.46 / 37.42 / 24.67 / 13.32 / 5.37 per mille; the 30-per-mille constraint and the 60-second publish constraint intersect at a single value, 60 seconds.
  • At a watermark of zero, most of what is missed is not late-arriving data but a normal event that lands in the window’s final seconds; at 15 seconds, this share disappears entirely.
  • The scan window is squeezed from both sides: twenty-four hours violates the workload constraint at 5.00 times, six hours violates the closing constraint at 1.046 per mille; twelve hours satisfies both at the margin.

Course Wrap-Up

Fourteen cases carried out the same six steps, and each was defended with a number.

Case The number that decides the design Patterns chosen Eliminated alternative What is given up
URL Shortening read/write 100; 3859.3 guesses at seven characters edge caching, cache-aside, sharding counter-based generation: 1.0 guesses per hit 300 s staleness window
News Feed fan-out on write 1691.87 / fan-out on read 56,570.71 work/s (33.44x) threshold-based hybrid fan-out, message queue, backpressure pure fan-out on read: 33.44x work 203.63 box writes per post
Search Suggestion 266.67 bytes per query; 4.59 GB at depth 10 prefix tree, task queue, push-based distribution a flat sorted list: 6593 units at p99 10-character limit, 600 s freshness
Content Distribution request ratio 3.13, byte ratio 20.83; required hit rate 0.7440 content delivery network, edge caching, versioned name short-lived single cache: 0.6188 at 2 GB atomicity of publishing
Chat System peak delivery 22,222.22/s; 1,250,000 connections, 25 nodes uniqueness key, sequential convoy, offline mailbox polling: 140.63x requests chat-level parallelism
Notification System single-channel delivery 0.9590, 3.01 points below the threshold cascading order, circuit breaker, bulkhead pattern parallel fan-out: 2.78x calls 30-60 s delivery delay
Rate Limiter fixed window 1200 (2.000x), sliding window counter 600 sliding window counter, 0.2 s delayed synchronization centralized counter: 40,000 touches/s overshoot staying at 1.015x
Ticketing and Inventory demand 56x the allotment; drains in 0.179 s pessimistic locking, allotment row optimistic locking: 2409 valid rejections 304,276 wait steps
Payment Flow notification-only 59.74 per mille; with polling 9.76 per mille idempotency, 15-min reconciliation cycle, audit trail synchronous verification: 75.77 per mille 9.76 per mille and 30-min closure
Object Storage 1.0075 at a 2 MB part; erasure (10,4) factor 1.40 valet key, idempotency, sharding, supervisor three copies: 3.00x, 438 lost per year 697,680 GB-month of extra storage
Video Streaming watched/uploaded 225; 8.91 percent not playable priority queue, claim check, edge caching eight rungs: 2.58x, 1854 processors bandwidth utilization 64.9 percent
Metrics Aggregation 240,000 samples/s; 31.50 TB against 393.57 TB wide-column store, time partitioning, materialized view five-minute rollup only: 100 percent error at the threshold percentile questions older than 7 days
Location-Based 864 candidates at a 1 km cell, 51.4 percent hit rate spatial index, composite index, hash sharding 8 km cell: 13,611 candidates a scan roughly half wasted
Batch and Stream 24.67 per mille at a 60 s watermark; 0.287 per mille at 12 hours watermark, event log and projection, idempotency stream only: the divergence never closes 3.00x work, two code paths

The table’s rule is this curriculum’s rule: a design decision can be defended only together with a number and an alternative. A decision given without writing down the number’s class — assumption, calculation, measurement — cannot be argued, because the other side does not know what to ask for a reason: an assumption is defended with its rationale and sensitivity, a calculation with arithmetic that has been run, a measurement with its apparatus and seed.

Seven courses built this rule piece by piece. Introduction to System Design turned qualities into numbers and set up the back-of-the-envelope estimate; the Traffic Layer took on how a request enters the system, the Application Layer and Service Interaction how work is split across services, and Scaling the Data Layer how data is distributed. Resilience and Reliability took on how failure is contained, and Performance Anti-Patterns and Monitoring showed how these are made visible. Case Studies combined all of them, one problem at a time, and asked for the same thing every time: turn the constraint into a number, write down the assumption, run the calculation, choose the pattern by name, take on one alternative, say what is given up.

What comes after this is the reader’s own design problem. The constraints there will not resemble these fourteen cases’; the only thing that will resemble them is the method. The first question is always the same: which number decides this choice, what class is that number, and which alternative was eliminated.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close