The War Story
What you will be able to do
One question, many wordings
- When did you last speed up a slow job, and how?
- What is the hardest performance problem you have debugged?
- Walk me through a pipeline you are proud of.
- A job of yours missed its SLA. What did you do?
- Have you ever cut the cost of a Spark job?
What gets graded
Choosing the story
- You made the change yourself and measured it
- The cause is a Spark mechanism: a shuffle, a join strategy, skew or a partition count
- Before and after come from the same job on comparable input
- You can say what the Spark UI showed before you touched the code
- A teammate found the cause and you deployed the fix
- The fix was a bigger cluster, and nobody knows why it helped
- The only evidence is that it felt faster
- A rewrite of 20 jobs where no single change can be named
Choose the headline figure first
| Fact | Value |
|---|---|
| Job | store_revenue_nightly |
| Output | Revenue per region per day, last 90 days |
| Large input | orders: 9 billion order lines, 410 GB of Parquet |
| Small input | stores: 1,200 rows, 3 MB |
| Cluster | 40 executors, 4 cores each |
| Window | Starts 04:00, dashboards refresh 05:00 |
| Runtime | 94 min before, 21 min after |
| Cause | Broadcast joins disabled by a shared config template |
Priya's headline: 'Our nightly store revenue job took 94 minutes against a 60-minute window. I found a config line forcing a shuffle join against a 3 MB table, took it out, and the job now runs in 21 minutes.' It names the job, the before, the after and the cause in under 40 words, and it leaves the interviewer wanting the middle.
- ▸List 3 stories from your own work where a job got faster, cheaper or stopped failing.
- ▸Next to each, write its cause in Spark terms in a single line. Cross out any you cannot.
- ▸Take the survivor and write its headline: the job, the before, the after, the cause.
- ▸Say the headline aloud with a timer. Under 12 seconds is the target.
Where the first minutes go
| Segment | Slow opening | Fast opening |
|---|---|---|
| Headline sentence | none | 8 s |
| Company and team | 60 s | left out |
| Tech stack | 50 s | named only where the fix touches it |
| History of the job | 70 s | left out |
| Who uses the output | 60 s | a clause: the 05:00 dashboards |
| Time before the action | 240 s | 28 s |
| Action starts at | not reached | 0:28 |
The slow column is the opening Priya gave in the interview from the intro. At 4 minutes she still had not reached the change, and the interviewer had to ask for it.
The fast opening, word for word
Tell the action as a chain of evidence
Say 'I' for your part
- ▸Write your context in 2 sentences and under 45 words, keeping the deadline.
- ▸Write your action as a chain: noticed, looked, found, changed, measured.
- ▸Beside each link, write the Spark UI tab and the figure it showed.
- ▸Time headline plus context aloud and cut until the action starts before 0:30.
Two measurements of one job
| Night | Before (min) | After (min) |
|---|---|---|
| 1 | 91 | 20 |
| 2 | 97 | 22 |
| 3 | 94 | 21 |
| 4 | 88 | 19 |
| 5 | 103 | 24 |
| 6 | 92 | 21 |
| 7 | 96 | 20 |
| Median | 94 | 21 |
| Metric | Before | After | Where it is read |
|---|---|---|---|
| Runtime, median of 7 runs | 94 min | 21 min | Jobs tab, Duration |
| Join strategy | SortMergeJoin | BroadcastHashJoin | SQL tab, plan graph |
| Shuffle exchanges | 3 | 1 | SQL tab, Exchange nodes |
| Stages in the main job | 4 | 2 | Jobs tab, job detail |
| Slowest stage | 68 min | 19 min | Stages tab, Duration |
| Shuffle write | 140 GB | about 1 MB | Stages tab, Shuffle Write |
| Spill (Disk) | 61 GB | 0 | Stage detail, summary metrics |
The old plan, on a small sample
The animation runs the old job's shape on 12 order lines and 4 stores, each table read as 3 partitions. It groups by region alone to keep the board small; the real job grouped by region and day. With broadcasting off, Spark must bring each order line to the same task as its store, so both tables move. Watch both inputs get shuffled by store_id, then a third shuffle as groupBy brings each region to a single task.
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", -1)revenue = (orders.join(stores, "store_id").groupBy("region").agg(F.sum("amount").alias("revenue")))revenue.collect()
The new plan, same sample
Here is the same sample with the broadcast hint. Spark collects stores on the driver and sends a copy to every executor, so each orders task looks up its stores locally and joins inside the scan. Watch the order rows stay on their tasks until the groupBy.
revenue = (orders.join(F.broadcast(stores), "store_id").groupBy("region").agg(F.sum("amount").alias("revenue")))revenue.collect()
Show the numbers did not change
The FULL OUTER JOIN keeps rows that exist in only one table, and IS DISTINCT FROM treats a NULL against a value as a difference, so a region missing from either version shows up as well as a changed sum.
| region | order_date | old_revenue | new_revenue |
|---|---|---|---|
| West | 2024-03-10 | 18240.50 | NULL |
| North | 2024-03-11 | 22915.00 | 22890.00 |
- Quote each metric with the Spark UI tab it came from
- Compare several runs on the same input size
- Name the shuffle that remains after the fix
- Show the output matched with a reconciliation query
- Pit one lucky run against one bad run
- Give a ratio like 4.5x with no minutes behind it
- Claim savings you never measured
- Skip spill when the slow stage was spilling
- ▸Write your before-and-after as a table: runtime, exchanges, shuffle write, spill.
- ▸Next to each figure, note the Spark UI tab you would read it from.
- ▸Decide how you would show the output did not change, and write that query.
- ▸If a number is missing, say in the interview that you did not measure it.
When they ask you to walk through the system
Order lines flow from the tills into orders every hour. Each night the job reads 90 days of orders plus all of stores and overwrites store_revenue, which the dashboards read at 05:00.
Sizes are what make the tour useful
When you do not remember a size
The tour, in the order data flows
- Sourcestore tillsHourly files of order lines, about 100 million lines a day across 1,200 stores.
- Inputsorders and storesorders holds 410 GB for the 90 days the job reads; stores is a 3 MB dimension, a row per store.
- The jobstore_revenue_nightlyJoins orders to stores on store_id and sums amount by region and day, on 40 executors with 200 shuffle partitions.
- Outputstore_revenueSix regions times 90 days, 540 rows, overwritten each night.
- Consumerregional dashboardsRefresh at 05:00, an hour after the job starts, which sets the deadline.
Finish on the box that shuffled
- Names each input with its size
- Says the deadline once
- Marks where the shuffle happens
- Stops at the consumer
- Lists tools with no sizes
- Explains the company's data strategy
- Covers every job in the pipeline
- Never says where the fix sits
- ▸Draw your story's system in at most 6 boxes.
- ▸Write the size under each input: rows, bytes or both.
- ▸Mark the transformation you changed and the shuffle inside it.
- ▸Give the tour aloud in 30 seconds, upstream to downstream.
The follow-up that decides the answer
Before: both tables travel
A sort-merge join needs each order line on the same task as its store. With broadcasting off, Spark got there by hash-partitioning both tables on store_id into 200 partitions. Each scan task wrote its rows to local disk, split by partition. Each of the 200 reducer tasks then fetched its partition from every scan task, sorted it by store_id and merged it with the matching stores rows. For orders that meant 140 GB written, 140 GB fetched and roughly 700 MB sorted per task, which spilled.
After: only the small table travels
A broadcast hash join brings the small table to the data. The driver collects stores, sends a copy to each of the 40 executors, and each executor builds an in-memory hash table keyed by store_id. Every orders scan task probes that hash table as its rows stream past. No order line is written or sent anywhere until the groupBy, and by then each task has collapsed its rows into a few partial sums.
- Moves both tables: 140 GB of orders, plus stores
- Adds 2 exchanges for the join
- Sorts every order line by store_id
- Risk: reducer tasks spill when partitions are large
- Fits: 2 large tables
- Moves stores only: a copy per executor
- Adds no exchange for the join
- Sorts nothing; probes a hash table
- Risk: the small side must fit in driver and executor memory
- Fits: a small table that stays small
The questions that come next
- Why not add executors?hardwareMore executors split the 140 GB over more tasks, but every byte is still written, fetched and sorted, and the bill grows with the cluster. Removing the movement beat spreading it.
- Why not raise the shuffle partitions?spillAt 800 partitions each reducer sorts about 175 MB instead of 700 MB, so spill shrinks. The 140 GB still crosses the network, so it eases the symptom and keeps the cause.
- What is the shuffle that is left?the groupByThe sum by region and day. Each scan task sends its partial sums, about 1 MB in total, and 200 tasks finish them.
- What if stores keeps growing?headroomIt gains a few dozen rows a year. At 3 MB it is about a third of the default threshold and far below what driver or executor memory can hold.
- Why was broadcasting off at all?historyAnother job hit an out-of-memory error during a broadcast, and the template author disabled automatic broadcasts for everyone. I raised it with the template's owners, since changing it for every job needed its own testing.
- Did the results change?correctnessNo. A week of side-by-side output, reconciled by region and day, differed in 0 rows.
Predict the plan before you claim it
revenue = (orders.join(stores, "store_id").groupBy("region").agg(F.sum("amount").alias("revenue")).orderBy(F.desc("revenue")))revenue.collect()
Two exchanges and 3 stages. The join broadcasts without a hint because stores is under the default threshold, so it adds no shuffle. The groupBy shuffles partial sums by region. The orderBy adds a range shuffle and a stage: Spark samples the revenue values to pick boundaries, then sends each row to the task that owns its range, so the collected rows arrive in order. Cutting the sorted result to its first few rows would let Spark run a top-N and skip that range shuffle.
- ▸Write the mechanism of your fix in 3 sentences: what moved before, what moves now, what it cost.
- ▸Pick 4 follow-ups from the list above and answer each for your own story in 2 sentences.
- ▸Change a single line of your job's code on paper and predict its exchanges and stages.
- ▸Explain the fix aloud to someone who has not seen the job. Note where they get lost.
> A food delivery marketplace runs a nightly courier pay job that joins 30 days of delivery events, about 1.2 TB, to a table of pay rates per city zone, then sums pay per courier per day for a payroll export at 06:00. When a rate-history column was added, the rates table grew to 14 MB, crossing the 10MB default threshold. Spark planned a sort-merge join, began shuffling the 1.2 TB of events every night, and runtime went from 50 minutes to 130, past the export. The engineer added a broadcast hint on the 14 MB table and runtime fell to 46 minutes. Months later she is asked in an interview about a time a job missed its SLA.
Tell a Spark speedup story so the interviewer hears the number, the shuffle you removed and why it worked.
- Category
- Spark
- Difficulty
- beginner
- Duration
- 15 minutes
- Challenges
- 3 hands-on challenges
Topics covered: One Story, One Number, Get to Your Action Fast, The Before and After, The System Tour, "Why Did That Work?"
Lesson Sections
- One Story, One Number (concepts: Spark UI Diagnosis)
One question, many wordings This pattern arrives in many forms, and every form asks for the same 3 things: a real Spark job you changed, what was wrong with it, and evidence that the change helped. Train your ear on the shape under the wording, because the interviewer will rarely say 'war story'. The last 2 sound like different topics. They are the same request: an SLA miss is a runtime problem, and a Spark cost cut almost always comes from fewer bytes shuffled or fewer executor-hours. A well-ch
- Get to Your Action Fast (concepts: Broadcast Join)
Where the first minutes go Many war stories are lost before the engineering starts. The candidate covers the company, the team, the stack and the job's history, and the interviewer, who has budgeted a few minutes for the whole answer before follow-ups, hears none of the work being graded. Context earns its place only when the listener needs it to follow your action. For a Spark speedup that is 2 facts: what the job produces, and why its runtime mattered. The slow column is the opening Priya gave
- The Before and After (concepts: Broadcast Join)
Two measurements of one job A before-and-after compares the same job on comparable input, read from the same place each time. A before from a run over 60 days of data against an after over 90 says nothing, and an interviewer who asks 'were those the same input?' will catch it. Priya took 7 nightly runs on each side, every one rebuilding 90 days, and quoted the median. The spread matters as much as the median. Before runs ranged from 88 to 103 minutes and after runs from 19 to 24, so the slowest
- The System Tour (concepts: Pipeline Monitoring)
When they ask you to walk through the system A common first follow-up is 'can you describe the system around that job?' The interviewer is placing your fix: what feeds the job, how large each input is, where the output lands and who waits for it. A tour of about 30 seconds that names the sizes lets them see the problem before you explain it, which is the strongest place a war story can put a listener. Six boxes cover it. The tour leaves out the file layout, the scheduler and the monitoring, beca
- "Why Did That Work?" (concepts: Broadcast Join)
The follow-up that decides the answer Most war stories end with some version of 'why did that work?' It separates a fix you understood from one you copied. The answer is a mechanism with 3 parts: where the data moved before, where it moves now, and what that movement cost. Before: both tables travel After: only the small table travels The questions that come next Expect 2 or 3 of these after the mechanism, and prepare a 2-sentence answer to each for your own story. Predict the plan before you cl