A Spark application has one driver that builds a logical plan (a DAG of
RDD/DataFrame transformations) and a set of executors that actually hold
data partitions in memory and run tasks. Transformations like map or
filter are lazy — Spark just records them. Nothing runs on
the cluster until an action like collect() or
count() forces execution.
groupByKey or a join) needs matching keys together, so partitions are hashed and redistributed across executors over the network — the most expensive step in a Spark job..cache()/.persist() gives on iterative workloads.Because Spark keeps intermediate results in executor memory instead of writing every stage to disk (as classic MapReduce does), iterative algorithms — like gradient descent over many epochs — can be an order of magnitude faster, since only the first pass pays the cost of materializing the cached data.
A driver coordinates a ring of executors as data partitions flow through a map stage, a network shuffle and a reduce stage — nothing happens until you fire the action that triggers Spark's lazily built DAG.
Narrow transformations run in place on each executor with zero network cost; wide transformations force a shuffle, redistributing partitions by hash key before a reduce stage aggregates and the driver collects the result.
Set the executor and partition counts, then press Run job to trigger the lazy DAG. Toggle in-memory caching to see mapped partitions stay resident on their executor for the next run instead of recomputing.
The shuffle is Spark's most expensive operation because it moves data across the network between every executor pair — minimizing shuffles is one of the biggest levers for speeding up a real Spark job.