Data Contracts

A data contract is an explicit, versioned agreement between the team that produces a dataset and the teams that consume it. It turns "please don't rename that column" from a hallway request into something a machine can check. This page explains what goes into a contract, how it is enforced, how schemas evolve without breaking people, and where contracts fit next to data quality tools and catalogs.


1. What a Data Contract Is

A Data Contract Sits Between Producer and Consumers Producer service orders-api owns the source Data contract v2 schema + semantics SLAs + owner + policy Analytics / dbt ML features Finance reports publishes The contract, not the producer's internal tables, is the interface consumers build on.

In most companies, analytics pipelines read directly from tables owned by application teams. Those teams change their schemas for good product reasons, and nobody tells the downstream dashboard, model, or finance report until it breaks. A data contract fixes the root cause: it makes the dataset an interface with a named owner, the same way a REST API is an interface.

The contract is usually a YAML or JSON file stored in the producer's repository. It describes what the data looks like, what it means, how fresh and complete it will be, and who to call when it is wrong. The producer promises to honour it; consumers build only on what the contract guarantees, never on undocumented columns that happen to exist.

2. Why It Matters

Pipelines rarely fail because of bad code. They fail because an upstream assumption silently changed: a column became nullable, a status enum gained a value, an amount switched from cents to dollars, or a late-arriving batch was skipped. Every one of those is a change in the interface between two teams. Without a contract, the consumer discovers it in production, often days later, and the fix involves backfills and apologetic emails.

Contracts move that discovery to the producer's pull request, where the change is cheap to discuss. They also make ownership explicit, which is the precondition for a data mesh style organisation where domains publish data as a product.

3. What Goes in a Contract

Anatomy of a Contract Schema fields, types, nullability, keys Semantics meaning, units, allowed values Service levels freshness, volume, availability Governance owner, PII tags, versioning policy Enforced in CI (before deploy) and at runtime (before data lands) a contract nobody checks is just documentation

Schema is the structural part: field names, types, nullability, primary keys. It is necessary but not sufficient, because a schema says nothing about meaning. Semantics fill that gap: what one row represents (an order, not a payment attempt), units and currencies, allowed values, and business rules such as "amount is never negative".

Service levels describe delivery: freshness (newest record no older than one hour), completeness, expected volume, and availability. Governance covers the owner, the classification of sensitive fields, retention, and the versioning policy. Open specifications such as the Open Data Contract Standard (ODCS) define a common layout for all of these so tools can read the same file.

4. How Enforcement Works

Where Enforcement Happens Producer PR schema diff vs registered contract CI gate breaking change? block or bump major Ingestion gate validate each batch or message Trusted table consumers read Quarantine bad rows + alert Shift-left: catch it before it ships

A contract is only valuable if something checks it. There are two enforcement points. The first is design time: when a producer opens a pull request, CI compares the proposed schema with the registered contract. Additive changes pass; breaking changes fail unless the producer bumps the major version and agrees a migration window with consumers. Schema registries for Avro or Protobuf on Kafka apply exactly this idea to event streams.

The second is run time: every batch or message is validated before it lands in the trusted zone. Rows that violate the contract go to a quarantine table and raise an alert to the producer, because they own the fix. Consumers then read only validated data, and the checks double as the data quality tests for that dataset.

5. Schema Evolution Rules

Contracts use semantic versioning. A useful rule of thumb for which changes are safe:

ChangeBackward compatible?Version bump
Add an optional (nullable) columnYesMinor
Add a value to a documented enumUsually not; consumers may switch on itMinor with notice, or major
Make a nullable column requiredYes for readersMinor
Make a required column nullableNoMajor
Rename or drop a columnNoMajor, run old and new in parallel
Change type or unit (cents to dollars)No, and silently dangerousMajor, new column name

For breaking changes the standard pattern is expand and contract: publish v3 alongside v2, let consumers migrate within an agreed deprecation window, then retire v2. Never change units or meaning in place; add a new field with a new name instead.

6. Worked Example: Contract and Validator

The contract below describes an orders dataset. It is deliberately small but contains all four parts.

# orders.contract.yaml  -- one file per published dataset
id: urn:contract:sales:orders
version: 2.1.0                 # semver: MAJOR = breaking
owner: team-checkout@example.com
description: One row per placed customer order (not per payment attempt).
schema:
  - name: order_id
    type: string
    required: true
    unique: true
  - name: customer_id
    type: string
    required: true
    pii: false
  - name: amount
    type: decimal(12,2)
    required: true
    description: Order total in the order currency, tax included.
    checks: [{min: 0}]
  - name: currency
    type: string
    checks: [{allowed_values: [USD, EUR, GBP]}]
  - name: created_at
    type: timestamp
    required: true
sla:
  freshness: 1h          # newest row no older than 1 hour
  completeness: 99.9%    # rows delivered vs rows produced
compatibility: backward  # consumers on 2.x keep working

A minimal validator reads the contract and checks a pandas DataFrame against it. Production tools (Soda, Great Expectations, dbt model contracts, or the datacontract CLI) do the same thing with more checks and better reporting.

import yaml, pandas as pd

TYPES = {"string": "object", "timestamp": "datetime64[ns]"}

def validate(df: pd.DataFrame, contract_path: str) -> list[str]:
    c = yaml.safe_load(open(contract_path))
    errors = []
    for f in c["schema"]:
        col = f["name"]
        if col not in df.columns:
            if f.get("required"):
                errors.append(f"missing required column {col}")
            continue
        if f.get("required") and df[col].isna().any():
            errors.append(f"{col}: {df[col].isna().sum()} nulls")
        if f.get("unique") and df[col].duplicated().any():
            errors.append(f"{col}: duplicate keys")
        for chk in f.get("checks", []):
            if "min" in chk and (df[col] < chk["min"]).any():
                errors.append(f"{col}: values below {chk['min']}")
            if "allowed_values" in chk:
                bad = set(df[col].dropna()) - set(chk["allowed_values"])
                if bad:
                    errors.append(f"{col}: unexpected values {sorted(bad)}")
    return errors

df = pd.DataFrame({
    "order_id": ["o1", "o2", "o2"],
    "customer_id": ["c1", "c2", None],
    "amount": [19.99, -5.00, 12.00],
    "currency": ["USD", "EUR", "JPY"],
    "created_at": pd.to_datetime(["2026-10-01"] * 3),
})
for e in validate(df, "orders.contract.yaml"):
    print("VIOLATION:", e)

Running it reports a duplicate order_id, a null customer_id, a negative amount, and an unexpected JPY currency. In a real pipeline those rows go to quarantine and the producer team gets the alert. Notice that the currency violation is exactly the "enum gained a value" change that breaks downstream reports without anyone noticing.

7. Contracts vs Related Practices

PracticeWhat it guaranteesWho actsGap a contract fills
Schema registry (Avro/Protobuf)Structural compatibility of eventsProducer at publish timeNo semantics, no SLAs, no ownership
Data quality testsChecks on data already landedUsually the consumerDetects breakage after the fact
Data catalogDiscovery and documentationData stewardsDescriptive only, not enforced
dbt model contractsColumn names and types of a dbt modelAnalytics engineerLimited to the warehouse layer
Data contractSchema, meaning, SLAs, owner, versioningProducer, enforced in CI and runtime-

8. Pitfalls

10. Further Reading