Part III shifts from theory to practice. Chapters 5–9 built the distributed systems vocabulary (replication, partitioning, transactions, failure models, consensus). Now we see how real systems actually crunch data at scale. Chapter 10 covers batch processing — the oldest form of computation, from punch cards to MapReduce to Spark.
The central insight: batch processing turns the distributed systems nightmare on its head. Instead of worrying about individual request latencies, you treat the cluster as a single machine that reads input, runs a deterministic job, and writes output. The framework handles partitioning, fault tolerance, and scheduling — your code is just a pure function over records.
Mission tie-in: Every system design interview for data-intensive roles (Stripe, Uber, Netflix, LinkedIn) will probe your understanding of batch vs stream vs online processing. When the interviewer says "let's design a recommendation system" or "how would you build a search index from scratch?", you're in Chapter 10 territory. The Lambda Architecture debate, the MapReduce v/s MPP divide, and materialized views (data cubes, OLAP cubes) all trace back to this chapter.
Kleppmann gives us a clean taxonomy. Every data system falls into one of three categories:
| Type | Also called | Input | Output | Key metric | Latency |
|---|---|---|---|---|---|
| Services | Online systems | Request | Response | Response time / availability | ms–seconds |
| Batch processing | Offline systems | Bounded input (files) | Derived output (files) | Throughput | minutes–days |
| Stream processing | Near-real-time | Unbounded stream (events) | Derived output (stream/files) | Latency + throughput | seconds–minutes |
The web (and HTTP/REST in general) has made the request/response model so dominant that we forget the other two. But batch processing is how Google built its search index, how LinkedIn computes "People You May Know", how Netflix generates recommendations, and how every credit card company detects fraud overnight.
Kleppmann starts with a deceptively simple example: analyzing an nginx access log. A single log line looks like:
216.58.210.78 - - [27/Feb/2015:17:55:11 +0000] "GET /css/typography.css HTTP/1.1" 200 3377 "http://martin.kleppmann.com/" "Mozilla/5.0 ..."
To get the five most popular URLs:
cat /var/log/nginx/access.log |
awk '{print $7}' |
sort |
uniq -c |
sort -rn |
head -n 5
This chain of Unix commands — awk, sort, uniq -c,
sort -rn, head — processes gigabytes of logs in seconds on a single
machine. It's the prototype for MapReduce. The key insight:
sort is the workhorse that brings related data together.
uniq -c then counts adjacent duplicates — which is exactly what a reducer does.
The Ruby equivalent keeps a hash table in memory:
counts[url] += 1. Which is better? It depends on the
working set (number of distinct keys):
The Unix pipe model is the conceptual ancestor of MapReduce. Four principles from Doug McIlroy (1964):
sort is a better sorting implementation
than most language standard libraries (it spills to disk, uses multiple threads).
In Unix, everything is a file descriptor — a sequence of bytes. An actual
file, a pipe, a device driver (/dev/audio), a TCP socket — they all share the
same interface. This is what makes composition possible.
By convention, records are separated by \n (newline). Fields are split by
whitespace. This is ugly but universal — it's why awk, sort, and
head can interoperate even though they're written by different people.
Contrast with today: you can't pipe your email into a spreadsheet and post the result to a social network. Data balkanization is the norm, not the exception.
A Unix program reads stdin and writes stdout. It doesn't know or
care where input comes from or output goes to. This is loose coupling /
inversion of control. The shell user wires things up.
Four properties make Unix tools great for exploration:
less anywhere in the pipeline.
The biggest limitation: Unix tools run on a single machine. Hadoop/MapReduce extends this model to thousands of machines.
MapReduce is Unix tools, distributed. A MapReduce job is like a Unix process: it takes
immutable inputs, produces outputs, and has no side effects. The key difference: instead of
stdin/stdout, MapReduce reads and writes files on a
distributed filesystem — HDFS (Hadoop Distributed File System, an open-source
implementation of Google File System / GFS).
| Feature | HDFS | NAS/SAN |
|---|---|---|
| Architecture | Shared-nothing | Shared-disk (centralized appliance) |
| Hardware | Commodity machines + conventional network | Custom hardware + Fibre Channel |
| Metadata server | NameNode (tracks block → machine mappings) | Centralized controller |
| Fault tolerance | Replication (default 3x) or erasure coding (Reed-Solomon) | RAID |
| Data locality | Computation scheduled near data (rack-aware) | Computation must fetch over network |
HDFS scales to tens of thousands of machines with hundreds of petabytes. The key to scale: data is stored on the same machines that process it — computation near data (also called locality optimization). If erasure coding is used, locality advantage is lost because data from multiple machines must be combined to reconstruct a file.
A MapReduce job has four steps, which exactly mirror our Unix pipeline:
A single MapReduce job is rarely enough. For the "top 5 URLs" example:
Jobs are chained via directory names in HDFS. Job 1 writes to
/output/intermediate/, Job 2 reads from it. This is
materialization
— writing intermediate state to durable storage. It's like Unix pipes writing to temp files
instead of using in-memory buffers.
Workflow schedulers (Oozie, Azkaban, Luigi, Airflow, Pinball) manage dependencies between jobs. Complex workflows (e.g., LinkedIn's recommendation system) use 50–100 MapReduce jobs per run.
MapReduce has no indexes — it does full table scans. For analytic queries over large datasets, this is acceptable (and often desirable, since sequential I/O is fast). But it means joins must be implemented differently than in an OLTP database.
The classic reduce-side join. Example: joining user activity events with user profiles.
Key insight: mappers "send messages" to reducers. The key is the destination address. All messages with the same key arrive at the same reducer. This separates physical network communication from application logic — the application never deals with partial failures, retries, or network issues.
Same mechanism as a join, but only one input dataset. Use cases:
The "all same key → same reducer" pattern breaks for hot keys (linchpin objects, e.g., a celebrity with millions of followers). One reducer gets swamped while others sit idle. Solutions:
Reduce-side joins are general but expensive (sorting + shuffling + merging). If you can make assumptions about the input, map-side joins avoid the reduce phase entirely. Each mapper reads one input file block and writes one output file — no sorting, no shuffling.
The simplest map-side join. One input must be small enough to fit in memory on each mapper machine. The mapper loads the small input into a hash table, then scans the large input and looks up each record.
Both inputs are partitioned the same way (same key, same hash function, same number of partitions). Each mapper loads only its partition of the small input. This allows larger inputs than a broadcast join, because each mapper loads less data.
Both inputs are partitioned and sorted the same way. The mapper reads both inputs incrementally (like merging two sorted lists) and produces the join output. No hash table needed — works for arbitrarily large inputs.
Why run all these jobs? Batch output is usually not a report for humans. It's data for other systems.
Google's original use case for MapReduce. A workflow of 5–10 MapReduce jobs builds the inverted index: mappers extract terms from documents, reducers build postings lists. The output is an immutable set of index files. To update: rerun the entire workflow (or use incremental indexing with Lucene segment files).
Batch jobs often build databases: recommendation models (people you may know), classifiers (spam filters), related products.
Don't write to an external database from inside a mapper/reducer! Three problems: (1) network overhead kills throughput; (2) parallel writes overwhelm the target database; (3) partial job failures leave externally-visible side effects, breaking the all-or-nothing guarantee.
Correct approach: build the database files themselves inside the batch job and output them to HDFS. Then load them in bulk into a read-only serving layer (Voldemort, Terrapin, ElephantDB, HBase bulk loading). The files are immutable once written. Voldemort atomically switches: copy new files to serving nodes, then swap in one operation. If anything fails, old files are still there.
Batch processes follow the Unix philosophy:
This enables human fault tolerance: if you deploy buggy code that produces wrong output, you roll back the code and rerun. You don't need to "fix the data" as you would with a database that has read-write transactions. This makes Agile development of data pipelines feasible — mistakes aren't irreversible.
MapReduce wasn't new — MPP databases (Teradata, Gamma, Tandem NonStop SQL) had parallel join algorithms a decade earlier. The differences:
| Aspect | MPP Databases | Hadoop/MapReduce |
|---|---|---|
| Storage | Proprietary, schema-on-write | Raw files, any format, schema-on-read |
| Processing | SQL only (optimized) | Any program (MapReduce, then Hive, Pig, Spark, etc.) |
| Data model | Must model before importing | Dump data first, figure out schema later ("data lake") |
| Fault tolerance | Fail entire query, restart | Retry individual tasks |
| Memory/disk | Keep in memory (hash joins) | Eagerly write to disk (fault tolerance + sort-based) |
| Best for | Short analytic queries (seconds–minutes) | Long batch jobs (minutes–hours) |
Hadoop's "dump data first, model later" approach is called the data lake (or enterprise data hub). It's the opposite of the careful schema-on-write of traditional data warehouses. The sushi principle: "raw data is better."
This shifts the burden of interpretation from producer to consumer (schema-on-read). It enables ETL: dump transactional data into HDFS, then MapReduce jobs clean and transform it into relational form for an MPP data warehouse.
SQL can't express everything: machine learning feature engineering, natural language models, image analysis, recommendation algorithms. MapReduce gave engineers the ability to run arbitrary code over large datasets. This led to an explosion of processing models on top of the same HDFS data: Hive (SQL), HBase (random access), Impala (MPP-style), Spark (in-memory), Flink (streaming), Mahout (ML), Giraph (graphs) — all accessing the same files. This is the Hadoop ecosystem's killer feature.
Why is MapReduce so eager to write to disk and so tolerant of task failures?
At Google, MapReduce tasks run at low priority in mixed-use datacenters. Production services and batch jobs share machines. If a higher-priority task needs resources, a low-priority batch task can be preempted (killed) at any time. A task that runs for an hour has ~5% risk of being preempted. A 100-task job with 10-minute tasks has >50% chance of at least one preemption.
Key insight: MapReduce's design is optimized for frequent task termination caused by resource overcommitment, not hardware failures. This allows better cluster utilization by "picking up scraps under the table."
MapReduce is robust but slow — especially because of materialization of intermediate state: every job writes to HDFS and the next job reads it back. This has three problems:
Dataflow engines fix materialization by treating the entire workflow as one job, with explicit DAGs of operators. They're like Unix pipes (streaming intermediate data through memory or local disk) instead of Unix temp files.
| Feature | MapReduce | Dataflow engines |
|---|---|---|
| Intermediate state | Materialized to HDFS (full replication) | In-memory or local disk |
| Operators | Strict map → shuffle → reduce | Flexible DAG of operators |
| Pipeline execution | Stage must finish before next starts | Pipelined (start as soon as input available) |
| Sorting | Always between map and reduce | Only where needed (partitioned hash joins skip sort) |
| Task startup | New JVM per task | Reuse existing JVMs |
| Fault tolerance | Read from HDFS (durable) | Recompute from lineage (RDDs) or checkpoint |
Since intermediate state isn't in HDFS, failures require recomputation. Spark uses RDDs (Resilient Distributed Datasets) — an abstraction that tracks the lineage (how each partition was computed). If a partition is lost, it's recomputed from the original input or a prior checkpoint.
Determinism matters: operators must be deterministic for correct recovery. Non-deterministic sources (random numbers, system clock, hash table iteration order) must be handled carefully. Nondeterminism cascades — if a restored operator produces different output, downstream operators must also be killed and re-run.
Many graph algorithms (PageRank, shortest paths, transitive closure) are iterative: they repeat until convergence. MapReduce can't express this efficiently — it would read the entire input dataset on every iteration, even if only a small part changed.
The Bulk Synchronous Parallel model (popularized by Google's Pregel paper) is designed for iterative graph processing. Implementations: Apache Giraph, Spark GraphX, Flink Gelly.
Caveat: graph algorithms often have high network overhead (message passing). If a graph fits on a single machine, a single-threaded algorithm will likely outperform a distributed Pregel job. The COST (Configuration that Outperforms a Single Thread) paper makes this point forcefully.
Raw MapReduce is too laborious (you implement join algorithms from scratch). Higher-level APIs emerged:
The trend is toward declarative query languages. Cost-based optimizers choose join algorithms automatically (sort-merge v/s broadcast hash v/s partitioned hash). Declarative operators enable columnar storage optimizations (Parquet) and vectorized execution (tight inner loops over CPU cache-friendly data).
Meanwhile, MPP databases are becoming more flexible (user-defined functions, support for ML libraries like MADlib). The two worlds are converging.
Chapter 10 builds a bridge from Unix pipes to distributed batch processing. The key ideas:
Select the correct answer for each question.
Scoring: 8/10 = Mastery. 6/10 = Review. <6/10 = Reread sections.
1. What distinguishes batch processing from services and stream processing?
Bounded input dataset and throughput as the primary measure Availability is the primary performance measure Unbounded input and millisecond-level latency2. In the Unix log analysis pipeline, what step corresponds to the MapReduce shuffle + sort?
awk '{print $7}' sort uniq -c3. Why does GNU sort handle larger-than-RAM datasets better than an in-memory hash table?
It uses more CPU cores by default It spills to disk using mergesort with sequential I/O It avoids spilling to disk entirely4. What is the uniform interface that enables Unix tool composition?
ASCII text records separated by newlines A file descriptor — an ordered sequence of bytes Shared memory for inter-process communication5. How does the MapReduce framework ensure all key-value pairs with the same key reach the same reducer?
Round-robin across all available reducers The mapper explicitly specifies the target reducer for each KV pair Hash of the key to determine the reducer partition6. In a reduce-side sort-merge join, how does the reducer distinguish user profile records from activity event records?
Each reducer handles only one key and one input type A secondary sort ensures the user profile record arrives before the activity events The reducer queries the user database for each key it processes7. What condition makes a broadcast hash join feasible?
One join input is small enough to fit in memory on each mapper machine Both inputs must be sorted by the join key Both inputs must have the same number of partitions8. Why is writing to an external database from inside a MapReduce reducer a bad idea?
The data must be read from the distributed filesystem first The mapper output must be sorted before the reducer can process it Network overhead, DB overload from parallelism, and loss of all-or-nothing semantics from partial job failures9. What is the main performance advantage of dataflow engines (Spark, Flink, Tez) over MapReduce?
They use less memory since they don't store intermediate state They avoid materializing intermediate state by pipelining data through memory or local disk They don't use HDFS at all and work exclusively with local files10. According to the COST paper and DDIA, when should you avoid distributed graph processing (Pregel)?
Only when the graph has more than a billion vertices When the graph can fit in memory or on disk of a single machine When the graph algorithm is not deterministicServices → request/response, latency-critical (ms)
Batch → bounded input, throughput-critical (min–days)
Stream → unbounded events, near-real-time (sec–min)
a) Do one thing well → sort, awk, each sharp tool
b) Uniform interface → file descriptor = ordered byte sequence
c) Sep of logic & wiring → stdin/stdout, shell wires
d) Transparent → immutable inputs, inspect any stage, no side effects
e) Limitation → single-machine only
a) InputFormat → split files into records
b) Mapper → key + value per record; stateless; called once per record
c) Shuffle → hash(key) → partition; sort by key; merge
d) Reducer → called once per key with iterator over all values
Unix → stdin/stdout pipes (in-memory buffer)
MR → HDFS files (materialized intermediate state)
Dataflow engines → pipe-like in-memory/local-disk streaming
MR always sorts between map and reduce; dataflow skips sort where not needed
a) Sort-merge → reduce-side; general; shuffle + sort both inputs
b) Broadcast hash → map-side; 1 input fits in memory; mappers load hash table
c) Partitioned hash → map-side; both inputs same partitioning; per-partition hash table
d) Map-side merge → both inputs partitioned + sorted; merge like two sorted lists
Hot keys → skewed join (sample → random reducers), two-stage aggregation
Immutability → rerun safely, rollback code, reroll output
Human fault tolerance → buggy code → fix code + rerun, no data corruption
Correct output method → build DB files inside job, bulk-load into read-only serving layer
Wrong output method → write to external DB from reducer (overwhelms DB, breaks atomicity)
Hadoop → schema-on-read, data lake, any program, retry tasks, eager disk writes
MPP → schema-on-write, SQL only, fail entire query, memory-optimized hash joins
Convergence → batch engines add SQL optimizers; MPP DBs add UDFs + ML libs
Preemption → low-priority tasks killed by higher-priority ones (Google Borg model)
5%/hour preemption risk at Google → MR designed for frequent task death
Resource overcommit → better utilization, "pick scraps under the table"
Hardware failure rate × → preemption rate dominates
Spark (RDD lineage), Flink (pipelined streaming), Tez (YARN shuffle)
Fix: materialization overhead, straggler delay, redundant mappers, over-replication
Fault tolerance → recompute lost partitions from lineage or checkpoint
Determinism required for correct recovery × random/clock/iteration order
BSP → supersteps: compute → send messages → barrier sync → next superstep
Stateful vertices → remember state across iterations (unlike stateless mappers)
Checkpoint at superstep boundary for fault tolerance
COST paper → single-thread beats distributed Pregel for single-machine graphs
Primary source: Kleppmann, M. (2017).
Designing Data-Intensive Applications, Chapter 10: “Batch Processing.”
O'Reilly Media.
Recommended supplements: Dean & Ghemawat's original MapReduce paper (OSDI
2004), the Hadoop: The Definitive Guide (Tom White), the Spark paper (Zaharia et al., NSDI
2012), the COST paper (McSherry et al., HotOS 2015), the Pregel paper (Malewicz et al., SIGMOD
2010), and the FlumeJava paper (Chambers et al., PLDI 2010).
Ask follow-up questions to drill into: how HDFS NameNode handles metadata at scale, the exact format of the shuffle (partition, sort, spill, merge), how Spark RDD lineage enables fault recovery, why Pregel supersteps act as global barriers, the difference between at-most-once/at-least-once/exactly-once in batch processing, or how the sushi principle ("raw data is better") actually plays out in production data lakes.