ETL and Data Pipelines · Running Pipelines in Production

Data Quality Checks and Data Contracts

How a renamed column upstream turned revenue into zero while every job stayed green, and how automated checks and data contracts catch bad data before people do.

Scenario

Example company: the payments team renames amount to amount_cents in their service's export as part of a refactor. Nobody tells the data team.

What you see in production

  • The pipeline loads successfully. The amount column is now NULL in every new row.
  • The revenue dashboard shows a cliff to zero. A sales director spots it in a meeting two days later.
  • Every job in the orchestrator shows green.

The naive code

row["amount"] = record.get("amount")      # missing field becomes None, silently

Reproduce it

The same batch is checked by a set of simple data-quality rules: required columns present, null rates, value ranges and volume compared with recent history.

Automated checks on an incoming batchTry it yourself
history_rows = [10_120, 9_870, 10_410, 10_050, 9_990]         # rows per day, last five days
batch = [{"order_id": i, "amount": None, "currency": "USD"} for i in range(1, 10_201)]
expected_columns = {"order_id", "amount", "currency"}
contract_columns = {"order_id": "int", "amount_cents": "int", "currency": "str"}   # what the producer now sends

checks = []
missing_in_contract = expected_columns - set(contract_columns)
checks.append(("schema matches contract", not missing_in_contract, f"missing upstream: {sorted(missing_in_contract)}"))
null_rate = sum(r["amount"] is None for r in batch) / len(batch)
checks.append(("amount null rate < 1%", null_rate < 0.01, f"null rate {null_rate:.0%}"))
avg = sum(history_rows) / len(history_rows)
checks.append(("row count within 30% of average", abs(len(batch) - avg) / avg < 0.3, f"{len(batch)} vs avg {avg:.0f}"))
ids = [r["order_id"] for r in batch]
checks.append(("order_id unique", len(ids) == len(set(ids)), "duplicates found"))

for name, ok, detail in checks:
    print(f"{'PASS' if ok else 'FAIL'}  {name}" + ("" if ok else f"  ({detail})"))
blocking = [n for n, ok, _ in checks if not ok]
print("publish blocked:" if blocking else "published", blocking if blocking else "")

Output

FAIL  schema matches contract  (missing upstream: ['amount'])
FAIL  amount null rate < 1%  (null rate 100%)
PASS  row count within 30% of average
PASS  order_id unique
publish blocked: ['schema matches contract', 'amount null rate < 1%']

The row count looked normal, so volume monitoring alone wouldn't have caught it. The schema and null-rate checks did.

Root cause

The pipeline only checked that code ran, not that the data was right. And the agreement about what the payments export contains existed only implicitly, so a producer change silently broke a consumer.

The fix

  1. Automated data tests after each load, before data is published: schema, not-null, uniqueness of the grain key, accepted values, referential integrity, ranges, freshness and volume compared with history. dbt data tests and tools like Great Expectations or Soda run these.
  2. Severity levels: error blocks publishing (zero revenue), warn notifies the owner (slightly more nulls than usual).
  3. Data contracts: the producing team publishes a versioned schema with semantics and guarantees (fields, types, meaning, freshness, owner) and treats breaking changes like API changes: versioning, notice and a migration window. See the Data Contract specification.
  4. Quarantine bad rows or batches instead of loading them, so good data still flows.

Trade-offs

  • Too many noisy checks get ignored. Start with grain, nulls on key columns, freshness and volume, and add checks after each incident.
  • Contracts need producer buy-in; they work best when producers own and test them in their CI.

Detect it in production

  • Track test results over time per table, with an owner for each failing test.
  • Monitor distributions (null rate, averages, category counts) for drift, not just hard rules.

Key points

  • A green pipeline only means the code ran; test the data before publishing it.
  • Block on critical failures, warn on the rest.
  • Data contracts make producer schemas explicit and turn breaking changes into planned migrations.

Practice questions

Which data quality checks would you run first on a new table?

Answer

Uniqueness and not-null on the grain key, not-null on critical measures, freshness (newest timestamp), volume versus recent history, accepted values for status fields, and referential integrity to dimensions.

What is a data contract?

Answer

An explicit, versioned agreement from a data producer about the data it provides: schema, types, meaning, quality and freshness guarantees, and an owner. Breaking changes follow a deprecation process instead of happening silently.

Why can a pipeline be 'green' while the data is wrong?

Answer

Orchestrators report whether tasks ran without errors. Missing fields, NULLs, duplicates or empty extracts don't raise errors unless you test the data itself.

Open in the app to mark lessons complete, take notes and track your progress.