SwadhyAI Notes

All System Design notes

System Design · Data Processing

Batch Processing Algorithms

Forty billion events landed yesterday. Finance needs revenue by SKU, ML needs user features, and compliance needs a closed ledger. None of that must update mid-scroll—but it must finish before morning with correct aggregates. That is : finite inputs, staged algorithms, durable intermediate results.

This note is the algorithm catalog behind those jobs: , , , hash and sort-merge joins, combiners, skew, and recovery via / lineage. Stream processing is the continuous cousin; here the dataset has an end.

One nightly aggregate

1

Parallel map over file splits or table partitions.

2

Pre-aggregate when the reduce is associative.

3

Move partials to the reducer that owns the key.

4

Reduce / write

Final aggregate, join, or sort into output files.

What actually happens

Batch engines schedule a DAG (or a linear MapReduce job) over . Embarrassingly parallel map/filter stages need little coordination. Group-by, join, and global sort force a : data is repartitioned by key, often spilled through . That shuffle is where cost and skew live.

Joins pick an algorithm from sizes and memory: broadcast , partitioned/grace hash join, or . Combiners and partial aggregations shrink bytes before they cross the network. Failures replay from durable map outputs or from lineage; explicit trade rewrite cost for shorter recovery.

The product promise is different from streaming: you optimize for correct completion by a deadline (SLA), not for millisecond watermarks. Micro-batches blur the line; the same algorithms still sit under the stage planner.

How to choose batch algorithms

1. Embarrassingly parallel? Map/filter/project only—no shuffle. Keep it that way if the business question allows.

2. Aggregate or join by key? Budget shuffle. Add a combiner if associative; pick hash versus sort-merge from memory and sortedness.

3. Skewed keys? Plan salting or skew joins before the first 8-hour incident.

Simple rule of thumb

Prefer fewer shuffles, smaller shuffles, and even partitions. Materialize when retry cost exceeds write cost. Treat p99 task time as the stage clock.

MapReduce as the template

Diagram text: the exact Mermaid definition below preserves the nodes, labels, and connections. Enable JavaScript to view the rendered diagram.
Read the diagram definition
flowchart LR
    In[Input splits] --> Map[Map tasks]
    Map --> Comb[Optional combiner]
    Comb --> Shuf["Shuffle / sort by key"]
    Shuf --> Red[Reduce tasks]
    Red --> Out[Output partitions]

made the template explicit. Map emits intermediate key/value pairs. An optional reduces locally. The framework partitions by key, sorts each partition (often via ), and calls reduce once per key (or per key group). of map outputs is why a dead reducer does not force every mapper to rerun from source—though a dead mapper still does.

plaintext
Word count (classic):
  map(doc):
    for word in tokenize(doc): emit(word, 1)
  combine(word, counts[]):   # optional, associative
    emit(word, sum(counts))
  reduce(word, counts[]):
    emit(word, sum(counts))

Cost intuition:
  map CPU ∝ input bytes
  shuffle bytes ≈ size of map output after combine
  reduce CPU ∝ per-key group size
  stage wall time ≈ max(task times)  # stragglers dominate

Modern engines (Spark, Flink batch, BigQuery, warehouse SQL) hide the API but keep the stages: exchange operators are shuffles; hash aggregates are combiners plus final merge; sort operators are external sorts. Reading a query plan is reading MapReduce in nicer clothes.

External sort and why reducers wait

When intermediate data exceeds RAM, engines sort with replacement-selection or quicksort-sized runs, write runs to disk, then k-way merge. Network shuffle often lands unsorted chunks that must be sorted before a streaming reduce can assume key order. That is why “CPU is fine but the job is slow” often means disk-bound merge.

plaintext
External sort sketch:
  while input remains:
    fill memory with records
    sort → write run_i to disk
  merge runs with a k-way heap (or multi-pass if too many runs)

I/O lower bound: roughly 2 passes over data for sort
  (write runs + read for merge), more if merge is multi-level

Design knobs:
  more memory → fewer/larger runs → cheaper merge
  more spill disks / better local SSDs → faster shuffle
  fewer giant partitions → less per-task sort pressure

Join algorithms

Diagram text: the exact Mermaid definition below preserves the nodes, labels, and connections. Enable JavaScript to view the rendered diagram.
Read the diagram definition
flowchart TD
    J[Equi-join two datasets] --> Fit{Smaller side fits memory?}
    Fit -->|Yes| HJ[Broadcast / in-mem hash join]
    Fit -->|No| Part[Partition both on join key]
    Part --> Fit2{Per-partition build fits?}
    Fit2 -->|Yes| GH[Grace / partitioned hash join]
    Fit2 -->|No| SM[Sort-merge join]
    HJ --> Skew{Key skew?}
    GH --> Skew
    SM --> Skew
    Skew -->|Yes| Salt[Salt hot keys / skew join]
    Skew -->|No| Done[Run stage]
    Salt --> Done

builds on the build side and probes with the stream side. Broadcast hash join ships the small side to every executor—excellent until the “small” side is not. Grace/partitioned hash join first partitions both inputs on the join key so each partition’s build fits memory.

sorts both sides and merges. It shines when inputs are already sorted (storage order, prior stage) or when hash tables would thrash. Many optimizers switch strategies with statistics; bad stats pick broadcast for a 200 GB “dimension” and melt executors.

AlgorithmBest whenWatch out
Broadcast hashBuild side fits executor memoryMisestimated size → OOM
Partitioned hashBoth large; enough memory per partSkewed key → one fat partition
Sort-mergeHuge sides; ordered output usefulSort I/O cost; still skew-sensitive
Skew / salted joinKnown hot keysExtra fan-out and bookkeeping

Combiners, partial aggregates, and associativity

A is a map-local reduce. Sums, counts, min/max, and sketch merges (HLL union) are safe. Average is not a safe one-shot combine unless you emit (sum, count). Distinct counts need sketches or a two-phase unique-then-count. Getting this wrong silently returns plausible wrong numbers—the worst batch failure mode.

plaintext
Safe combine:     sum, count, min, max, HLL.merge
Unsafe one-shot:  mean, median, distinct count (exact)
Pattern for mean:
  map → emit (key, sum=x, count=1)
  combine/reduce → sum sums, sum counts
  final → sum/count

Two-phase distinct:
  stage1: map unique (key, user) → shuffle → distinct users per key
  stage2: count per key
  or: use HyperLogLog merge in one stage if approx OK

Skew, stragglers, and barriers

Stages are : the slowest task defines finish time. A from key skew (one user_id with 5% of events) holds everyone. Mitigations: better partitioners, key salting with fan-in, range repartition from sampled quantiles, speculative execution of slow tasks, and isolating hot keys into dedicated reducers.

Also watch input skew: a few huge files versus millions of tiny ones. Compact small files in a prior batch; split large ones. Scheduling overhead and namenode/listing cost are real batch algorithms problems, not just “ops.”

Fault tolerance: materialize or recompute

Classic MapReduce materializes map output. Spark’s lineage recomputes lost partitions until you or persist at a wide dependency. Warehouse tables materialize each CREATE TABLE AS SELECT. Choose based on recomputation cost versus storage and on how often nodes die mid-flight.

plaintext
Recovery cost sketch:
  Job 4 hours, failure at 3:50 without checkpoint
    lineage recompute of expensive shuffle ≈ hours
  Checkpoint after heavy shuffle (~30 min write):
    failure at 3:50 → replay ~10 min of tail stages

Rule:
  checkpoint after expensive shuffles you refuse to redo
  do not checkpoint every tiny narrow transform

Exactly-once batch outputs:
  write to temp + commit (manifest rename / table transaction)
  never append partially visible files as “done”

Worked micro-estimate

plaintext
Daily events: 40e9 rows × 200 B ≈ 8 TB compressed input 2 TB
Group by (sku, country), sum revenue

Map + combine:
  cardinality of keys ≈ 5e6
  after combine, shuffle payload ≈ 5e6 × 32 B × map_tasks_factor
  suppose effective shuffle 80 GB

Cluster: 100 executors, 10 Gbps effective per host shared
  shuffle wall ≈ max(network, disk spill)
  80 GB across 100 hosts ≈ 0.8 GB/host ≈ seconds–minutes
    if even; if 1 key has 20% of bytes → that reducer dominates

Join to 20 GB dimension (product catalog):
  broadcast hash if 20 GB fits each executor memory budget
  else hash-partition both on sku

SLA: finish by 05:00 with 02:00 start → 3 h budget
  leave 30–40% headroom for stragglers and retries

Trade-offs to weigh

More parallelism versus shuffle overhead

Too few partitions leave cores idle and create fat tasks. Too many create tiny tasks, scheduler pressure, and small shuffle files. Size partitions from data volume and executor count, then adjust from stage timelines.

Materialize versus recompute

Materialization speeds retry and debug at the cost of I/O and delay. Pure lineage saves writes until a failure forces a deep replay. Checkpoint after the stages you cannot afford to redo.

Hash join versus sort-merge

Hash joins win with a modest build side and good memory. Sort-merge wins on huge-vs-huge and when order is free. Wrong broadcast decisions cause OOMs that look like “Spark is unstable.”

Exact distinct versus sketches

Exact distinct needs heavy shuffles or two phases. HLL-style sketches shuffle tiny state and accept error bars. Pick from the product’s tolerance, not from algorithm fashion.

Batch simplicity versus stream freshness

Batch algorithms are easier to reason about and cheaper for many analytics. If the answer must move with the user session, you need streaming—or micro-batches that still pay shuffle physics on a shorter clock.

Choosing and explaining batch processing algorithms

A complete explanation connects the mechanism to a requirement. Describe what improves when you introduce it, which new failure cases become possible, and how you would know that the trade-off is worthwhile.

Explain the core idea

Batch jobs run finite datasets through staged algorithms—map, shuffle, reduce, joins, and external sort—with clear completion.

Shuffle and skew dominate cost; combiners and join strategy exist to cut or tame that cost.

Fault tolerance is materialize-or-recompute; stage wall time follows the slowest task at the barrier.

When it belongs

Designing ETL, warehouse transforms, offline features, or large aggregations.

Explaining MapReduce, Spark stages, or why a join blew up memory.

Contrasting batch with stream processing for latency versus simplicity.

How it fails

Key skew and stragglers that stretch barriers to many times the mean task time.

Unsafe combiners that produce wrong aggregates that look plausible.

Broadcast joins on oversized build sides and uncheckpointed deep lineages.

Trade-offs to weigh

Shuffle parallelism versus scheduling and file overhead.

Hash versus sort-merge joins under memory and sortedness constraints.

Materialization cost versus recomputation risk on failure.

Common misunderstandings

Assuming more executors fix a single hot reducer.

Confusing wide-column or streaming engines with batch stage physics.

Treating mean task time as the ETA when p99 task time owns the barrier.

Wrap-up

Batch processing algorithms are how finite data becomes correct aggregates on a deadline: partition, combine, shuffle, join, sort, and commit. Optimize shuffle bytes and skew first, pick join algorithms from memory reality, and decide deliberately where intermediates materialize. Use the stream processing note when the dataset never ends.

Key takeaway

Batch is finite-input stage physics: cut shuffle, tame skew, choose joins you can fit, and materialize what you cannot afford to recompute—so morning SLA meets correct totals.