The same three-stage workflow (map → group-by → aggregate) over 100,000 records, two engines. The MapReduce-style engine writes each stage's full output to a replicated DFS — 2,918,670 bytes of intermediate state — and its first final record appears only after the entire input has flowed through all three stages (100,000 records consumed). The dataflow pipeline writes 0 intermediate bytes and emits its first record after just 101 — a 990× gap.
Run the SAME three-stage batch workflow — map → group-by → aggregate — over ONE dataset of 100,000 records, TWO ways. The MapReduce-style engine writes each stage's full output to a distributed filesystem before the next stage may read it: we model the DFS honestly (write real bytes, fsync, count 3 replica copies), so the intermediate data between stages is written 2,918,670 bytes — and because every stage's file must be complete before the next opens it, the first final record cannot appear until the entire input has passed through all three stages: 100,000 of 100,000 records consumed first. The dataflow-style engine chains the same three stages as generators: nothing is written between stages (0 bytes), and the first final record is emitted after only 101 input records — the first group plus the one record that closes it. Same workflow, same 1,000 aggregates; the only difference is whether intermediate state is materialized to disk or streamed in memory.
The prediction that fails: "writing between stages is a minor overhead." It is a full, 3×-replicated copy of the data at every stage boundary, and it imposes a barrier that forbids the stages from overlapping at all.
Designing Data-Intensive Applications, 2nd ed. (Kleppmann & Riccomini, 2025), Chapter 11 — Batch Processing, §"Materialization of Intermediate State" and §"Dataflow Engines":
Writing out this intermediate state to files is called materialization [...] a MapReduce job can only start when all tasks in the preceding jobs have completed, whereas processes connected by a Unix pipe are started at the same time.
The book contrasts MapReduce, which materializes every operator's whole output to replicated storage before the next reads it, against dataflow engines (Spark, Flink, Tez) that hand records operator-to-operator without landing them on disk. Here you measure both halves of that trade: the replicated intermediate bytes, and the barrier that delays the first output.
The result turns on what a stage boundary is in each engine. Each line notes where it shows up in the run.
materialize() writing each stage's output before the next stage reads it. Free resource: Dean & Ghemawat, "MapReduce: Simplified Data Processing on Large Clusters" (2004), §3 Implementation.* REPLICATION in the byte count: every intermediate byte is written three times. Free resource: Ghemawat et al., "The Google File System" (2003), §2.4 Replica Placement.run_mapreduce vs. run_pipeline. Free resource: DDIA 2e §"Dataflow Engines".(parse(raw) for raw in source) → streaming_group(...) → ((k, sum(vs)) ...) chain. Free resource: Python docs, "Generators" and PEP 289 (generator expressions).Same workflow, same data — only what happens between stages differs.
Both engines run the identical map → group-by → aggregate over the identical dataset and must produce the identical 1,000 aggregates. The whole thing is one script, code/materialization_vs_pipelining.py.
cd study/ddia/ch11/code
python3 materialization_vs_pipelining.py
Do not read the output yet — make each prediction first.
Predict. Two different engines run the same map → group-by → aggregate over the same 100,000 records. Should their 1,000 aggregates be identical, and roughly how big is the input to each?
Identical results — as they must be, it is the same computation. The 100,000 records are the shared input. The surprise isn't the answer; it's what each engine spends to get there.
Predict. The map output and the group output each get materialized to the DFS before the next stage reads them, and the DFS keeps 3 replicas. The pipeline writes nothing between stages. Is the materialized intermediate a "minor overhead," and what does the pipeline write — how many bytes each?
2,918,670 bytes vs. 0. Not a minor overhead: the map output and the group output are each a full copy of the data, and each is written 3× for replication. The pipeline never lands intermediate state on disk at all — those bytes live in memory and are gone the instant the next operator consumes them.
Predict. How many of the 100,000 input records must each engine consume before it can emit its first final aggregate? Same number for both, or different?
100,000 vs. 101. MapReduce's stage 3 (aggregate) cannot start until stage 2's file is complete, which cannot start until stage 1 has consumed every input record — so the first final record only exists after the whole input has flowed through all three stages. The pipeline pulls records through map → group → aggregate on demand: once the first group's 100 records are in and the 101st record (a new key) closes that group, the first aggregate falls out — 990× sooner.
A stage boundary in MapReduce is a file in a replicated distributed filesystem, and that single design choice produces both numbers from first principles.
The 3×. The map stage emits one (key, value) pair per input record; the group stage emits those same values, regrouped. Neither shrinks the data — the intermediate volume between stages is on the order of the input itself. The DFS then writes each block to REPLICATION = 3 machines so a lost disk loses no data. So the intermediate bytes are 3 * (map_output + group_output): two full stage outputs, each tripled. That is where 2,918,670 comes from — not a rounding error on the "real" work, but a quantity larger than the final result, paid at every boundary. Materialization buys fault tolerance with it: because each stage's input sits durably on disk, a crashed stage just re-reads its file and re-runs, with no need to recompute the stages before it.
The barrier. Materialization also serializes the stages. Stage 2 opens stage 1's file, so it may not start until that file is complete; stage 3 waits on stage 2 the same way. A file is complete only after its producer has consumed all of its input. Chain three such stages and the first final record cannot exist until the entire input has been read, mapped, written, re-read, grouped, written, re-read, and aggregated — hence 100,000 records consumed before the first output. The pipeline removes the files: each operator is a generator that yields to the next, so a record flows straight through, and the first aggregate emerges as soon as the first group is closed (record 101). No file means no barrier; nothing forces the engine to see the whole input before producing part of the answer.
Pipelining trades exactly what materialization bought. Its intermediate state is in RAM, so there is nothing to re-read after a crash: a failure forces recomputation from the last checkpoint or the source. Durability-by-materialization vs. latency-and-throughput-by-pipelining is the whole trade — the 3× write and the barrier are the price of the first, and their absence is the price of the second.
persist()/replicate anyway?Sources: DDIA 2e, Ch. 11, §"Materialization of Intermediate State", §"Dataflow Engines" · Dean & Ghemawat, "MapReduce" (2004) · Ghemawat et al., "The Google File System" (2003).