IntermediateReferenceData Engineering

Apache Spark

Apache Spark in practice - driver and executors, lazy evaluation and the DAG of stages, shuffles, partitions, Structured Streaming, and the failure modes that make Spark jobs slow or dead.

Applies to
Apache Spark3.5.x-4.xSpark 4.0 (May 2025) moved the baseline to JDK 17 and Scala 2.13, turned ANSI SQL mode on by default, and added spark.api.mode plus a Connect-enabled tarball for switching applications to Spark Connect; the execution model described here holds across both lines
establishedUpdated Reviewed

Apache Spark is a distributed compute engine: it takes a query expressed over data that does not fit in one process, compiles it into a graph of stages, and executes it across a cluster of executors coordinated by a driver. The driver plans; executors hold data partitions in memory and on disk and run the tasks. That split - one planner, many workers - is the whole architecture.

Lazy evaluation and the DAG

DataFrame and SQL operations are lazy: they build a plan, and nothing executes until an action (count, write, collect, show) forces it. The plan compiles into a DAG of stages separated by shuffles - the points where data must move between partitions.

The rule that decides performance: narrow dependencies (map, filter) pipeline within a partition and cost nothing; wide dependencies (join, groupBy, repartition) shuffle data across the network, and the shuffle is where jobs spend their time and memory. A job with one giant shuffle will be slower than a job doing ten times the computation with none. explain() shows you the physical plan - the stages and the exchanges between them - and it is the first place to look when a job is slow.

Partitions and parallelism

Spark reads data into partitions - the unit of work a single task processes. Too few partitions leave executors idle; too many drown the scheduler in task overhead. The practical symptom map:

  • Skew - one partition holds most of the data (one hot join key, one hot date). One task runs for hours while the rest finish in seconds. Salt the key, broadcast the small side, or split the skewed rows out.
  • Small-file amplification. Thousands of tiny input files produce thousands of tiny partitions and a schedule dominated by task overhead. Compact upstream, or coalesce on read.
  • Shuffle spill. A shuffle that does not fit in executor memory spills to disk, and a job that spills runs an order of magnitude slower. The fix is more partitions, more memory, or less data per key - not a bigger driver.

Structured Streaming

Structured Streaming runs the same DataFrame API over an unbounded source - Kafka, files, a socket - as a sequence of micro-batches (or continuously in the still-experimental continuous mode). Each micro-batch is a normal Spark job over new records; checkpointing tracks progress so a restarted job resumes where it stopped. State stores hold aggregates and joins across batches, and watermarking bounds how long late data is waited on - the two mechanisms that decide whether a streaming job keeps up or falls behind permanently.

Deployment and the ecosystem

Spark runs on its own standalone scheduler, YARN, Kubernetes, or as a managed service (Databricks, EMR, Dataproc, Synapse). Spark 4.0 pushed Spark Connect - a thin gRPC client that submits plans to a server rather than embedding the driver in your application - much further toward being the normal client: there is a Connect-enabled-by-default release tarball and a spark.api.mode switch that lets an existing application run over Connect, though the standard distribution still defaults to Classic. The practical consequence holds either way: the client-server split is what makes notebooks and thin clients practical against a shared cluster. The DataFrame API is the surface almost everyone should write against; RDDs remain for cases the optimiser cannot express, and pay for it by losing the optimiser entirely - and they are not available over Connect at all.

Spark versus the alternatives

  • Spark is the generalist: batch ETL, SQL, ML, and micro-batch streaming in one engine, the broadest ecosystem, and the most operational surface to own.
  • Apache Flink is the specialist for true event-at-a-time streaming: lower latency, richer event-time semantics, and a smaller operational community. If the workload is genuinely continuous, Flink is the more honest fit; if it is mostly batch with some streaming, Spark’s micro-batch model is usually enough.
  • Trino / Presto is a query engine, not an ETL engine: fast interactive SQL over whatever it reads, but no durable execution state for long transformations - a query that dies restarts from zero. Use it to query, Spark to build.

Failure modes and diagnostics

  • OOM on the driver from collect() or toPandas() on a large result - the driver is a single process and the classic way to kill it. Write or sample instead of collecting.
  • Shuffle fetch failures and executor loss under memory pressure - the stage retries, the job gets slower each retry, and eventually fails. Look at spill metrics in the UI before adding hardware.
  • Skew that looks like a hang. The UI shows one task at 99% for hours while the rest finished - that is skew, not a stuck cluster.
  • Streaming jobs that fall behind. Input rate exceeds processing rate per micro-batch and the lag compounds - the trigger interval and the state store’s size are the levers, and a checkpointed job that cannot keep up needs repartitioning, not restarting.

What this is not for

Spark is overkill for data that fits in one process - a pandas or Polars job on one machine is simpler, faster to write, and cheaper to run up to a genuinely large threshold. It is not a database: it reads and writes storage, it does not own a table format or enforce consistency for you - that is what Iceberg and its peers are for. And it is not an orchestrator: scheduling, retries across pipelines, and dependencies between jobs belong to Airflow and its peers, not to Spark itself.

Apache Spark release support

4 supported
ReleaseLatestReleasedEnd of lifeStatus
4.24.2.0Jul 2026Jan 2028Supported
4.14.1.3Dec 2025Jun 2027Supported
4.04.0.4May 2025Nov 2026Supported
3.5LTS3.5.9Sept 2023Nov 2027Supported
3.43.4.4Apr 2023Oct 2024End of life
3.33.3.4Jun 2022Dec 2023End of life

Data as of 2026-10-01 from endoflife.date. Refresh with node scripts/fetch-versions.mjs.

Where Apache Spark sits

How this component relates to the rest of the stack. Items without a link are not covered here yet.

Depends on

Must be present for this to run

Deployed with

Commonly run alongside