100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Lazy Evaluation and the DAG

How Spark defers execution of transformations into a logical plan and only runs work when an action forces it, using a DAG of stages for both optimization and fault tolerance.

Processing & OptimizationIntermediate8 min readJul 10, 2026
Analogies

Why Spark Waits Before It Works

When you call operations like map, filter, or select on a Spark DataFrame or RDD, Spark does not execute anything immediately. Instead, it records each transformation as a step in a logical plan, deferring real computation until an action such as count, collect, or write is invoked. This laziness lets Spark's optimizer see the whole chain of operations before running any of them, so it can reorder, combine, or skip steps to minimize actual work.

🏏

Cricket analogy: It's like a captain planning a full bowling rotation before the innings starts rather than reacting ball by ball — Dhoni would map out which bowler attacks which batsman for the whole spell, only committing once the field is set, so the plan can be adjusted before a single delivery is bowled.

Transformations vs. Actions

Transformations like filter, select, groupBy, and withColumn are narrow or wide operations that always return a new, immutable DataFrame without touching data yet. Actions like count, collect, show, and write force Spark to actually walk the accumulated plan and produce a result or side effect. Because nothing runs until an action fires, you can chain dozens of transformations cheaply, and Spark only pays the execution cost once, at the action boundary.

🏏

Cricket analogy: It's like a batting order card — naming Kohli at four and Gill to open costs nothing on paper, but runs are only scored once the umpire calls play and the first ball is bowled.

python
df = spark.read.parquet("s3://data/orders.parquet")

# Transformations: lazy, build up a logical plan
filtered = df.filter(df.amount > 100)
projected = filtered.select("customer_id", "amount", "order_date")
grouped = projected.groupBy("customer_id").sum("amount")

# Nothing has executed yet. Inspect the plan without running it:
grouped.explain(True)

# Action: triggers execution of the whole DAG
result = grouped.collect()

Building and Reading the DAG

As transformations accumulate, Spark's Catalyst optimizer converts them into a logical plan, then an optimized physical plan expressed as a Directed Acyclic Graph of stages. Each stage contains a pipeline of narrow transformations that can run on a partition without moving data across the network; stage boundaries occur wherever a wide transformation, like a groupBy or join, requires a shuffle. The DAG scheduler submits stages in dependency order, so downstream stages only start once their upstream inputs are materialized.

🏏

Cricket analogy: It's like a tour itinerary split into legs by venue changes — a team plays three consecutive matches in Mumbai without repacking, but a switch to Chennai forces a full travel and setup boundary before play resumes.

Call df.explain(True) (or EXPLAIN EXTENDED in SQL) to see the parsed, analyzed, optimized logical, and physical plans Spark built for your query — invaluable for confirming Catalyst chose the strategy you expected before you trigger an expensive action.

Stages, Shuffle Boundaries, and Fault Tolerance

Because Spark tracks the full lineage graph of how each RDD or DataFrame was derived, it doesn't need to replicate data for fault tolerance the way traditional distributed systems do. If an executor is lost mid-job, Spark simply recomputes the missing partitions by replaying the relevant portion of the DAG from the last available data, rather than restarting the entire pipeline. This is what makes lazy evaluation and the DAG a resilience mechanism, not just a performance optimization.

🏏

Cricket analogy: It's like DRS ball-tracking reconstructing a delivery's full path from release to impact — if the on-field call is unclear, the system replays the physics from known data points rather than bowling the ball again.

Chaining many transformations without ever caching an intermediate result means every action that touches that DataFrame replays the entire chain from the source. If you reuse a DataFrame across several actions, cache it explicitly — laziness alone won't save you from redundant recomputation.

  • Transformations (map, filter, select) are lazy and only build a plan.
  • Actions (count, collect, show, write) trigger real execution.
  • Catalyst compiles the logical plan into a physical plan expressed as a DAG of stages.
  • Stage boundaries form at wide transformations that require a shuffle.
  • Lineage lets Spark recompute lost partitions after executor failure instead of relying on replication.
  • Use .explain() to inspect the plan Spark built before it runs.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#ApacheSparkStudyNotes#LazyEvaluationAndTheDAG#Lazy#Evaluation#DAG#Spark#StudyNotes#SkillVeris#ExamPrep