Designing for Reprocessability

Concepts covered: Late-Arriving Data

The widest form of this question drops the symptom entirely. 'The billing provider is resending the whole of Q3 with corrected tax. What do you do?' Every technique so far is a special case of one capability: can you recompute any derived partition from raw whenever you choose and get the same answer the original run would have produced from the same inputs? A pipeline that can is reprocessable, and reprocessability is designed in, in 5 places, long before the resend arrives.

Raw keeps every arrival as it came. Everything to its right is recomputable from raw plus a declared window.
  • Device uploads (source, event batches)
  • raw_playback (storage, partitioned by processed_date)
  • Dedupe and place (transform, nightly job)
  • fact_playback (storage, partitioned by event_date)
  • Rollup hours by title (transform, nightly job)
  • Daily report (consumer, published 06:00)
  • Device uploads -> raw_playback
  • raw_playback -> Dedupe and place
  • Dedupe and place -> fact_playback
  • fact_playback -> Rollup hours by title
  • Rollup hours by title -> Daily report
  • Sources: Device uploads
  • Raw: raw_playback
  • Curated: Dedupe and place, fact_playback
  • Serving: Rollup hours by title, Daily report

5 design decisions

Reading the table as it was

Because every row carries processed_ts, the same fact_playback partition answers 2 different questions. 'How many plays happened on 14 October?' reads the partition as it stands today. 'How many plays did the 15 October report show for 14 October?' adds a single predicate on the arrival clock.

SELECT
  event_date,
  count(*) AS plays_as_known
FROM fact_playback
WHERE event_date = date '2026-10-14'
  AND processed_ts < timestamp '2026-10-15 06:00:00'
GROUP BY event_date

The second query reproduces a published number from a table the lookback has since rewritten 3 times. That holds because the rewrite keeps the first arrival per event_id, with its original processed_ts, and because corrections arrive as reversal rows with their own arrival clock rather than as edits. Remove either property and the as-of read is gone. A table with this property is bitemporal: one axis for when it happened, one for when you knew, and a report is a cut along the second.

Scoping the reprocessing request

Back to the Q3 resend. The answer is a blast radius computed from the lineage, and then a conversation with the people inside it.

StepWhat it touchesSize in the exampleWho must hear about it
Land the resendAppends 92 new raw partitions, old ones kept184 GB, 2 GB a dayNobody yet
Reverse changed chargesA reversal and a corrected row per changed charge, by `source_version`, over 92 charge dates2 rows per changed chargeOwners of downstream rollups
Rebuild rollupsMonthly revenue by region and tax by jurisdiction, 2 tablesRewrites 6 monthly partitionsFinance, before it runs
Publish a restatementOld and new totals side by side, from the as-of queryOne table of 92 rowsEveryone who read the Q3 numbers

Each step leans on a design choice above. Raw makes step 1 an append. Reversal rows guarded by source_version make step 2 a loop over 92 dates that is safe to rerun. The run log names the rollups that read those partitions. Step 4 works because each original amount stays in fact_charge beside its reversal, so an as-of cutoff on processed_date still returns the old total that a rebuild in place would erase. The compute takes hours. Without the old totals, the restatement conversation with finance takes weeks.

About this lesson

This section is part of the Late-Arriving Data: Advanced lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.