AdvancedPipeline Architecture · 38 min · 11 challenges

Data Quality: Advanced

PayPal published an open-source template for data contracts, and a Friday release on a checkout team shows what one is for. A checkout engineer renames amount_cents to amount and switches it to dollars. With a contract check in the build, the merge fails within minutes and gets fixed before lunch. Without one, Monday's revenue dashboard shows zero for every market, and finance spends the morning finding out why.

Data Contracts and CI Enforcement

Daily Life
Interviews
A data contract is a versioned file, owned by the team that produces a dataset, stating what every consumer may rely on. It has 3 parts. The schema lists each field with its type and whether it may be null. The guarantees state rules the values obey and delivery promises, such as how soon an event lands after the order completes. The evolution policy says which changes the producer may ship freely, which count as breaking, and how much notice consumers get first. The file sits in the producer's repository beside the code that emits the data, so a change to either is reviewed together.

A contract for one event

contract: checkout.order_completed
version: 1.2.0
owner: checkout-platform
schema:
- name: order_id
type: string
required: true
unique: true
- name: customer_id
type: string
required: true
- name: amount_cents
type: bigint
required: true
min: 1
- name: currency
type: string
required: true
allowed: [USD, EUR, GBP]
- name: completed_at
type: timestamp
required: true
- name: coupon_code
type: string
required: false
guarantees:
freshness_minutes: 60
daily_rows_min: 400000
amount_cents_meaning: total charged after discounts, before tax, in minor currency units
evolution:
additive: [add optional field]
breaking: [remove field, rename field, change type, make field required, change allowed values]
breaking_requires: new major version and 30 days notice

The version follows a major.minor.patch rule. An additive change, such as a new optional refund_reason field, raises it to 1.3.0 and asks nothing of consumers. A breaking change raises it to 2.0.0 and starts the 30-day notice. This team counts any edit to allowed as breaking, added values included, because a consumer that looks up exchange rates by currency fails on a code it has never seen. The amount_cents_meaning line matters as much as the types: a producer who starts including tax keeps every type valid and still moves every revenue figure downstream.

Enforcing it at 2 points

Continuous integration, or CI, is the set of automated checks that run on every proposed code change before it may merge. The contract check in the producer's CI builds a sample event from the changed code, usually inside a unit test, and diffs its fields against the contract file. If a pull request renames amount_cents to amount, the diff shows a required field missing and an undeclared field added. The check classifies that as breaking and fails the build unless the same pull request raises the contract to 2.0.0, which puts the rename in front of reviewers and starts the clock for consumers.

Build time
Run time
producer repo
Code change
CI runner
CI contract check
ERRORfail build on breaking diff
production service
Checkout service
event queue
Order events
pipeline task
Contract gate
ERRORquarantine batch
curated table
orders curated
SLA60 min
downstream readers
Finance and fraud

Code reaches production only through the CI check, and each batch of events reaches the curated table only through the gate. Both read the same contract file.

Multiple Choice

A checkout engineer adds JPY to the allowed currencies and raises the contract from 1.2.0 to 1.3.0 in the same pull request. What does the CI contract check do under this contract's evolution policy?

CI sees code, so it misses every problem the code did not cause. Suppose a payment partner starts routing orders through checkout with currency set to JPY by configuration. No code changed, so CI has nothing to check. The gate runs on each batch of real rows against the guarantees, so it catches the unknown currency, a day that lands 180,000 rows, or a replay that repeats order_id values. A failing gate quarantines the batch, and consumers keep reading the last good partition: the block authority from the beginner lesson, now driven by the contract.

SELECT
COUNT(*) AS row_count,
COUNT_IF(order_id IS NULL) AS null_order_id,
COUNT(*) - COUNT(DISTINCT order_id) AS duplicate_order_id,
COUNT_IF(amount_cents < 1) AS bad_amount,
COUNT_IF(currency NOT IN ('USD', 'EUR', 'GBP')) AS bad_currency
FROM staging.order_completed
WHERE batch_date = DATE '2026-10-23'
Multiple Choice

A refunds integration starts writing refunds into the order event stream as negative amount_cents values. Checkout's code has not changed. Which enforcement point stops these rows from reaching orders_curated?

Five Pillars of Observability

Daily Life
Interviews
A contract states what should be true about one dataset. Observability measures what is true across every table, including the many that have no contract yet. Data observability sorts those measurements into 5 pillars, each answering one question about a table. The same 5 serve 2 jobs: an order of questions during an incident, and a grid for deciding what a quality program covers.
PillarQuestionMeasured asTypical fault
FreshnessWhen did new data last land?minutes since the newest `loaded_at`a stalled upstream job
VolumeDid the expected amount arrive?rows per partition against recent historya dropped file, a doubled load
SchemaDid the shape change?column names and types against yesterdaya removed or retyped column
DistributionAre the values plausible?null rate, distinct count, percentilesa unit change, a flood of defaults
LineageWhat feeds this table, and what reads it?upstream and downstream datasets as a graphwhere a fault began and who it reaches
Lineage is the dependency graph between datasets: for any table, the tables its rows come from and everything built on it downstream, dashboards and models included. The first 4 pillars say that something is wrong with a table. Lineage says where to look upstream for the cause and whom to warn downstream.

The pillars as an incident order

Ask the pillars in order of cost. Freshness and volume come from load metadata in seconds, and together they split every incident in 2: data that did not arrive, and data that arrived wrong. Schema is a comparison of column lists. Distribution needs a scan of values, so it comes last. Lineage runs through all of it: test the symptom table first, then step upstream one table at a time until the failing pillar reads normal; the fault began in the next step downstream.
On Oct 22 the revenue dashboard shows 0 revenue for the EU market. Walking upstream, the pillars report this:
TableFreshnessVolumeSchemaDistribution
`revenue_daily`on timenormalunchangedEU revenue is 0
`orders_curated`on time31% below its 28-day medianunchangedno EU rows
`orders_raw`on time412,000 rows, normalunchangednew `currency` value `eur` in 31% of rows

Freshness is green everywhere, so the data arrived. Volume is normal in orders_raw and 31% low one step later, which places the loss in the step that builds orders_curated. That step inner joins orders to exchange rates on currency, and the distribution of orders_raw shows why rows vanish: a checkout release began sending lowercase eur, which matches no rate. Walking lineage downstream from orders_curated then finds a second victim, fraud_features, which no dashboard showed. A contract gate with a fixed currency list would have stopped this batch before orders_curated.

event queue
Checkout events
reference table
fx rates
raw table
orders raw
inner join on currency
orders curated
daily aggregate
revenue daily
feature table
fraud features
finance
Revenue dashboard
scoring service
Fraud model

Upstream from the dashboard, the fault first shows at orders_curated. Downstream from that table, fraud_features lost its EU rows too.

The pillars as a coverage map

To design a program, lay tables against pillars and mark each cell with whether an automated check covers it. Rank rows by how many people and systems read the table, then close gaps in the top rows first. Here is this pipeline as it stood before the incident:
TableFreshnessVolumeSchemaDistributionLineage
`orders_raw`yesyesyesgapyes
`orders_curated`yesgapyesgapyes
`revenue_daily`yesgapgapgapyes
`fraud_features`gapgapyesgapyes

The incident passed through 2 gaps: no distribution check on orders_raw and no volume check on orders_curated. Either one would have fired hours before finance noticed. revenue_daily has 3 gaps of its own, so a fault that started there would surface only on the dashboard.

Multiple Choice

The sessions_daily dashboard shows average session length doubled overnight. sessions_daily is fresh, its row count matches history and its schema is unchanged. What comes next?

Quality SLAs vs Ops SLAs

Daily Life
Interviews
The orchestration lessons treated an SLA as a promise that a run finishes on time. That is an operational SLA, a commitment about how the pipeline executes. A quality SLA is a commitment about the data a run publishes, and the 2 fail independently. During the EU currency incident every task succeeded and every table landed before 06:00, so the operational SLA held all day while consumers read wrong numbers. Treat them as 2 promises with separate measurements and separate owners, because fixing one does nothing for the other.
✓Operational SLA
  • Promises the partition is published by 06:00 UTC
  • Measured from run records: start, end, status
  • Broken by stuck tasks, slow inputs, cluster outages
  • Improved by retries, concurrency, earlier upstream schedules
✗Quality SLA
  • Promises the published partition is correct
  • Measured from check results and the corrections log
  • Broken by wrong values, missing rows, broken joins
  • Improved by contracts, producer-side checks, tuned thresholds

Writing each as a measurable commitment

A commitment names an indicator and sets a target for it over a window. The operational indicator is direct: was the partition of orders_curated published by 06:00 UTC the next day? For quality, passing checks is a weak indicator, because a check says nothing about defects it never tests for. A stronger one comes from the corrections log, a record written whenever anyone restates a published partition. The quality indicator becomes: was the published partition never corrected? A partition the gate holds back costs nothing on quality, since no consumer read it.

FieldOperational SLAQuality SLA
Indicatorpublished by 06:00 UTC the next daypublished and never corrected
Target89 of 90 days88 of 90 partitions
Windowrolling 90 daysrolling 90 days
Source`ops.runs``ops.corrections`
Fixed byplatform on-callproducer and table owner
WITH corrected AS (
SELECT DISTINCT
partition_date
FROM ops.corrections
WHERE table_name = 'orders_curated'
)
SELECT
COUNT(*) AS days,
COUNT_IF(
r.published_at <= CAST(r.partition_date AS TIMESTAMP) + INTERVAL '30' HOUR
) AS on_time_days,
COUNT_IF(c.partition_date IS NULL) AS never_corrected
FROM ops.runs AS r
LEFT JOIN corrected AS c
ON c.partition_date = r.partition_date
WHERE r.table_name = 'orders_curated'
AND r.status = 'published'
AND r.partition_date >= CURRENT_DATE - INTERVAL '90' DAY
Multiple Choice

On Oct 14 the gate blocks the orders_curated partition at 05:50 for duplicate order_id values. The owner fixes it and the partition publishes at 07:10. It is never corrected afterward. How does Oct 14 count?

90 of 90
days published by 06:00 UTC
84 of 90
partitions never corrected
The pipeline met its operational target with a day to spare and missed its quality target by 4 partitions. A blended health score would have averaged the 2 into a comfortable number and hidden the miss.
Where the 6 corrections came from
  • ▸One missing refund: a refund service outage dropped events, and no volume check covered refunds
  • ▸One EU join loss: lowercase eur matched no fx_rates row, and no distribution check covered currency
  • ▸2 duplicate orders: an upstream replay repeated rows, and the order_id uniqueness check was warn-only
  • ▸2 wrong conversions: fx_rates loaded a stale day, and no freshness check covered it
Every one of the 6 traces to a missing check or a check without blocking authority, and none shrinks with faster runs. Doubling retries lifts the operational number and leaves all 6 in place; a retry that reruns a bad batch sooner only publishes it sooner. The 2 SLAs also pull against each other. A blocking uniqueness check would have prevented 2 corrections, and every block holds a partition past 06:00 while someone fixes it. Write that tradeoff into the SLA on purpose: a held partition counts as late for operations and as protected for quality.
Multiple Choice

Over a quarter, payments_daily meets its operational SLA on 89 of 90 days and its quality SLA on 81 of 90. The team proposes doubling retries and adding a second cluster. What will that change?

Tuning Thresholds vs History

Daily Life
Interviews
A threshold decides how often someone gets pulled into an investigation. Set it too tight and people stop reading alerts; set it too loose and finance finds the incident first. The target is an alarm rate that fits the time the team has. Here, 3 engineers can each work about 5 quality alerts a week. Spread across 40 checks, that allows each check about 5 alerts per 90 days, and every threshold below is judged against that budget.

Backtest against 90 days of history

A backtest replays a candidate threshold over stored history and counts how often it would have fired. The distributional checks you already run produce one value per metric per day; store them in quality.metric_history and the backtest becomes a query. This one scores each day's row count for orders_curated against the mean and standard deviation of the 8 previous same weekdays, since a Sunday should be compared with other Sundays. It tries 4 values of k, the number of standard deviations that triggers an alert, over the last 90 days.

WITH scored AS (
SELECT
metric_date,
metric_value,
AVG(metric_value) OVER (
PARTITION BY DAY_OF_WEEK(metric_date)
ORDER BY metric_date
ROWS BETWEEN 8 PRECEDING AND 1 PRECEDING
) AS baseline_mean,
STDDEV(metric_value) OVER (
PARTITION BY DAY_OF_WEEK(metric_date)
ORDER BY metric_date
ROWS BETWEEN 8 PRECEDING AND 1 PRECEDING
) AS baseline_sd
FROM quality.metric_history
WHERE table_name = 'orders_curated'
AND metric_name = 'row_count'
AND metric_date >= DATE '2026-05-31'
)
SELECT
t.k,
COUNT_IF(
ABS(s.metric_value - s.baseline_mean) > t.k * s.baseline_sd
) AS alerts
FROM scored AS s
CROSS JOIN UNNEST(ARRAY[2.0, 2.5, 3.0, 4.0]) AS t(k)
WHERE s.metric_date >= DATE '2026-07-26'
GROUP BY t.k
ORDER BY t.k
The corrections log from the SLA work labels the answers. In this window, 4 of its entries changed the row count, so those 4 days are the incidents a row-count threshold should catch.
Before annotations
kAlertsIncidents caught
2.0234 of 4
2.5114 of 4
3.063 of 4
4.021 of 4
After annotations
kAlertsIncidents caught
2.0124 of 4
2.554 of 4
3.033 of 4
4.011 of 4

Before annotations, only k = 4.0 fits the budget, and it misses 3 of the 4 incidents. Most extra alerts at lower settings fired on events the team already understood, and those events also stretched the baseline for the 8 weeks after them, which made later real drops look smaller.

Annotate the anomalies you already know

An annotation is a row recording a deviation the team has already explained. It names the table and the reason, with a start and end date. Planned events get one before they happen, and unplanned ones get one after review. In both the backtest and production, annotated days stay out of the baseline and raise no alert.
TableStartEndReason
`orders_curated`2026-08-122026-08-12email campaign to all customers, volume 1.6x
`orders_curated`2026-09-182026-09-19sale over 2 days, volume 2.4x
`orders_curated`2026-10-052026-10-05app outage, real orders down 40%
SELECT
h.table_name,
h.metric_name,
h.metric_date,
h.metric_value
FROM quality.metric_history AS h
LEFT JOIN quality.annotations AS a
ON a.table_name = h.table_name
AND h.metric_date BETWEEN a.start_date AND a.end_date
WHERE h.table_name = 'orders_curated'
AND h.metric_name = 'row_count'
AND a.table_name IS NULL

After annotation, k = 2.5 catches all 4 incidents with 5 alerts, inside the budget. Give blocking checks a looser k than warning checks. A false warning costs an hour of reading. A false block holds a partition past 06:00 and spends the single spare day in the operational target.

✓Do
  • Backtest every new threshold against 90 days before it goes live
  • Compare each day with the same weekday in earlier weeks
  • Give every annotation an end date and a named owner
✗Don't
  • Copy one k across all 40 checks
  • Annotate a day only because it raised an alert
  • Let annotated days feed the baseline
Multiple Choice

After an alert nobody could explain, a colleague annotates orders_curated from 2026-10-20 with no end date. What follows?

Contracts on a Legacy Pipeline

Daily Life
Interviews

On a new dataset the contract comes first and the data is built to match. A legacy table reverses that order. orders_legacy has 23 columns and has loaded nightly for 6 years. It has no written owner, and nobody has listed its readers. A clean contract enforced on day one would block tonight's load. The rollout runs in 4 steps, and each produces something the next one needs.

Step 1: Write down what the table is today

Contract v1.0.0 describes the table as it behaves, quirks included, so it passes on its first night and gives every later step a baseline. Profile 90 days to find the facts.
SELECT
COUNT(*) AS row_count,
COUNT_IF(region IS NULL) * 100.0 / COUNT(*) AS region_null_pct,
MIN(amount) AS min_amount,
MAX(amount) AS max_amount,
ARRAY_AGG(DISTINCT status) AS status_values
FROM legacy.orders_legacy
WHERE order_date BETWEEN DATE '2026-06-28'
AND DATE '2026-09-25'
Profile findingEntry in contract v1.0.0
`region` null in 4.1% of rows`region` optional, null rate at most 5%
`status` has 7 values, with both `CANCELLED` and `canceled`all 7 values allowed, as written
`amount` is a `double` in dollars, minimum -310.00negatives allowed; meaning: refunds are stored as negative orders
`amount` maximum 48,200.00`amount` at most 50,000.00

Step 2: Inventory every consumer

A breaking change cannot be planned without knowing whom it breaks. Query history lists who read the table and which columns they touched. A 90-day search found 13 readers; the 14th, an annual audit export that reads once a year, appeared only when the search covered 13 months. Of the 14, 9 are scheduled jobs and 3 are dashboards; the other 2 are analysts' notebooks. Of those, 5 filter on lowercase canceled only, so normalizing status in place would drop cancellations from their counts without any error. Record an owner and a contact for each consumer; step 4 needs them.

Step 3: Observe before enforcing

Run every v1 rule in warn mode for 28 days and record each violation. A rule gets blocking authority once it has run clean or every violation has an explanation. The window from Sep 26 to Oct 23 produced this:
RuleViolations in 28 daysDecision
`region` null rate at most 5%0block
`amount` at most 50,000.00one, a confirmed 62,000.00 bulk order on Oct 3raise the bound in v1.1.0, then block
at least 300,000 rows a dayone, on Oct 5block, with Oct 5 covered by an `orders_legacy` outage annotation
`order_id` unique3stay warn until the upstream retry replaces its partition instead of appending, then block after a clean window

Step 4: Migrate behind a new major version

The cleanups go into contract v2.0.0 on a new table, orders_v2, under the same evolution policy as checkout's contract. There amount_cents becomes a bigint and status is normalized to 6 values. region is now required and filled from the delivery address. The legacy job writes both tables from the same input, each behind its own gate. Version 1 is frozen and receives fixes only. Each consumer from the inventory gets a dated move to v2, and orders_legacy is retired once every inventoried consumer has moved and the 30-day notice has passed.

Multiple Choice

Contract v2.0.0 maps both CANCELLED and canceled to CANCELLED. Of the 14 consumers, 5 filter on lowercase canceled. What keeps their cancellation counts right through the migration?

orchestrator
Nightly legacy job
contract v1.1.0
Gate v1
ERRORquarantine batch
contract v2.0.0
Gate v2
ERRORquarantine batch
frozen
orders legacy
retiring
new table
orders v2
not yet moved
Consumers on v1
moved on schedule
Consumers on v2

One job, 2 contracts, 2 gates. Each consumer moves from the v1 path to the v2 path on its own dated plan.

Multiple Choice

At week 6 of the move, 8 of the 14 consumers read orders_v2. A producer pull request renames region to market and raises the contract from 2.0.0 to 2.1.0. The volume check on orders_v2 has fired 6 times in those 6 weeks, against a budget of about 5 per 90 days. Which plan follows this lesson?

❯❯❯PUTTING IT ALL TOGETHER

> A grocery delivery company builds driver_payouts nightly from delivery events and pays drivers from it every Monday. The table is 4 years old, has 11 consumers and no contract. Last quarter 3 payouts were restated after the tips service started sending dollars where it had sent cents, while the job met its 05:00 deadline every night and the on-call engineer muted a volume alert that fired on every holiday.

The legacy rollout starts with v1 written from a 90-day profile, which records in the contract's meaning line that tips are in cents.
A unit change keeps the type, so CI lets it through. A distribution rule on tips in the pipeline gate catches it in the first batch, and the meaning line shows the change was breaking.
A coverage grid across the 5 pillars shows where the restatements got through: tips had schema checks and no distribution check.
The 3 restatements count against the quality SLA, separate from the operational SLA the job met every night, and faster scheduling would have prevented none of them.
Holiday annotations with end dates, plus a backtested k, bring the muted volume alert back inside what the on-call engineer can investigate.
Version 2 stores tips in a single unit under a new major version, and each of the 11 consumers from the inventory gets a dated move.
KEY TAKEAWAYS
A data contract has 3 parts: the schema and its guarantees, plus an evolution policy. All 3 live in the producer's repository and are versioned with the code.
CI enforces the contract against code changes, and the pipeline gate enforces it against real rows. Each catches failures the other cannot see.
The 5 pillars run from freshness and volume to schema and distribution, ending with lineage. They give a fixed order of questions in an incident and a grid of gaps when planning coverage.
An operational SLA counts on-time publication, and a quality SLA counts published partitions that never needed correction. Faster runs improve only the first.
Backtest thresholds against 90 days, comparing like weekdays, and annotate known events with end dates until alerts fit the team's capacity.
On a legacy table, document today's behavior as v1, list every consumer, observe in warn mode, then move readers to a new major version.

Write contracts producers can't break silently, measure quality as its own SLA, and tune alarms to team capacity.

Category
Pipeline Architecture
Difficulty
advanced
Duration
38 minutes
Challenges
11 hands-on challenges

Topics covered: Data Contracts and CI Enforcement, Five Pillars of Observability, Quality SLAs vs Ops SLAs, Tuning Thresholds vs History, Contracts on a Legacy Pipeline

Lesson Sections

  1. Data Contracts and CI Enforcement

    A data contract is a versioned file, owned by the team that produces a dataset, stating what every consumer may rely on. It has 3 parts. The schema lists each field with its type and whether it may be null. The guarantees state rules the values obey and delivery promises, such as how soon an event lands after the order completes. The evolution policy says which changes the producer may ship freely, which count as breaking, and how much notice consumers get first. The file sits in the producer's

  2. Five Pillars of Observability

    A contract states what should be true about one dataset. Observability measures what is true across every table, including the many that have no contract yet. Data observability sorts those measurements into 5 pillars, each answering one question about a table. The same 5 serve 2 jobs: an order of questions during an incident, and a grid for deciding what a quality program covers. Lineage is the dependency graph between datasets: for any table, the tables its rows come from and everything built

  3. Quality SLAs vs Ops SLAs

    The orchestration lessons treated an SLA as a promise that a run finishes on time. That is an operational SLA, a commitment about how the pipeline executes. A quality SLA is a commitment about the data a run publishes, and the 2 fail independently. During the EU currency incident every task succeeded and every table landed before 06:00, so the operational SLA held all day while consumers read wrong numbers. Treat them as 2 promises with separate measurements and separate owners, because fixing o

  4. Tuning Thresholds vs History

    A threshold decides how often someone gets pulled into an investigation. Set it too tight and people stop reading alerts; set it too loose and finance finds the incident first. The target is an alarm rate that fits the time the team has. Here, 3 engineers can each work about 5 quality alerts a week. Spread across 40 checks, that allows each check about 5 alerts per 90 days, and every threshold below is judged against that budget. Backtest against 90 days of history The corrections log from the S

  5. Contracts on a Legacy Pipeline

    Step 1: Write down what the table is today Contract v1.0.0 describes the table as it behaves, quirks included, so it passes on its first night and gives every later step a baseline. Profile 90 days to find the facts. Step 2: Inventory every consumer Step 3: Observe before enforcing Run every v1 rule in warn mode for 28 days and record each violation. A rule gets blocking authority once it has run clean or every violation has an explanation. The window from Sep 26 to Oct 23 produced this: Step 4: