Data Contracts
Quote
“A data contract is the API of data — it sets expectations between producers and consumers so that changes don’t break downstream systems silently.”
— Andrew Jones (data contracts advocate)
Summary
This note defines a data contract as the formal producer-consumer agreement over schema, semantics, ownership, service levels, and change management, then shows how those guarantees become real only when they are versioned, exported, and enforced in code, tests, and CI.
Contract structure and design workflow
- Explains the core parts of a contract such as schema, SLA, semantics, ownership, and versioning, then frames contract-first development as an agreement reached before implementation diverges.
- Compares schema definition formats and relates data contracts to API contracts so producer-consumer stability is treated as a deliberate interface problem.
Contracts in practice and enforcement
- Covers runtime-exported contracts, worked YAML examples, CI validation, and the three-layer enforcement model across ingestion validation, dbt tests, and deployment checks.
- Treats contracts as executable controls rather than passive documentation files.
Change management and responsibility boundaries
- Defines breaking versus non-breaking changes, major-version handling, and the responsibilities of producers and consumers once a contract exists.
- Uses anti-patterns to show how silent drift, unenforced YAML, and unclear ownership invalidate the whole contract model.
Operations and safety
- Warnings: a contract that is not tested will drift from reality, and breaking changes without controlled rollout move failures from build time into production.
- Recommendations: define contracts before implementation, enforce them at runtime and in CI, classify changes explicitly, and keep producer plus consumer obligations visible in version control.
Glossary
Data contract
A formal agreement that defines what a dataset contains, how it behaves operationally, and what producers owe consumers over time.
It matters here because the note treats data interfaces with the same rigor that software teams apply to APIs.
Agreement plus enforcement
A contract only protects consumers when the platform checks it automatically. Otherwise it is just a document that can fall out of sync with the data.
Schema
The structural definition of fields, types, nullability, keys, and related constraints for a dataset.
It matters here because schema is the most immediate part of the producer-consumer interface and the first surface where breakage appears.
Structure is not the whole contract
A stable schema is necessary, but it still does not tell consumers what fields mean, how fresh the data should be, or who to contact when it breaks.
SLA
The service-level commitment for freshness, availability, or quality thresholds that a data producer promises to meet.
It matters here because consumers often depend as much on timing and reliability as on field structure.
Freshness is part of the interface
A dataset that arrives structurally correct but late can still violate its contract and break downstream reporting or publication deadlines.
Semantics
The business meaning and interpretation rules attached to each field or dataset, beyond raw type information.
It matters here because consumers can still use a structurally valid dataset incorrectly if the meaning of values is unclear or shifts over time.
Same type, different meaning
A decimal column can remain a decimal across versions while its business definition changes completely. Semantics guard against this quieter class of breakage.
Ownership
The explicit identification of who produces, maintains, and supports the dataset when questions or incidents arise.
It matters here because a contract without an accountable owner leaves consumers with no escalation path when guarantees are missed.
Someone must own the incident
Ownership is operational, not ceremonial. It determines who investigates failures, approves changes, and communicates impact.
Semantic versioning
A versioning scheme that uses major, minor, and patch increments to signal the compatibility impact of changes.
It matters here because the note uses semver to distinguish controlled evolution from breaking changes that demand migration planning.
Version number is a promise
The version is useful only when the team applies it consistently. Otherwise consumers lose their only quick signal about compatibility risk.
Breaking change
A change that can cause an existing consumer to fail or behave incorrectly without modifications on their side.
It matters here because the note centers change classification as the key discipline that prevents silent downstream outages.
Additive is easier than subtractive
Removing fields, narrowing enums, or changing keys usually breaks consumers immediately, while additive changes are often survivable if contracts are designed well.
Contract test
An automated check that verifies produced data or declared schemas match the rules defined by the contract.
It matters here because automated verification is the mechanism that keeps contracts from drifting out of date.
Build-time consumer protection
Contract tests shift failures left by stopping incompatible changes before they land in shared environments or production loads.
Schema drift
Unplanned divergence between the expected contract and the data actually being produced by the upstream source or pipeline.
It matters here because data contracts exist largely to catch and manage this drift before it damages downstream consumers.
Silent drift is the real threat
The worst drift is not the one that crashes loudly. It is the one that keeps the pipeline green while changing the meaning or shape of the data underneath consumers.
What a Data Contract Contains
| Component | Definition | Example |
|---|---|---|
| Schema | Column names, types, nullability, constraints | instrument_isin CHAR(12) NOT NULL |
| SLA | Freshness, availability, quality thresholds | ”Available by 18:00 UTC, 99.9% uptime” |
| Semantics | Business meaning of each field | ”close_price is the official exchange closing price in local currency” |
| Ownership | Who produces, who maintains, who to contact | ”Market Data Team owns, Index Ops consumes” |
| Versioning | How changes are communicated and rolled out | ”Semver: breaking = major, additive = minor” |
The contract concept parallels API contracts in REST design — both define a stable interface between producer and consumer, with versioning and backward-compatibility guarantees.
Contract Without Enforcement
A contract YAML file that nobody validates in CI provides a false sense of safety. The contract will drift from reality within weeks: a column gets renamed, a type changes, an SLA shortens. Downstream consumers still break on schema changes — but now they’re SURPRISED because the contract said it wouldn’t happen. Enforce contracts automatically: Pydantic at ingestion, dbt tests at transform, CI checks at deployment. A contract that isn’t tested is a lie.
Enforce Contracts Automatically
Wire the contract into the pipeline at three points: (1) Pydantic model validation at ingestion time to reject non-conforming rows before they enter bronze; (2) dbt contract tests (
not_null,unique,accepted_values) that run on everydbt build; (3) a CI step in GitHub Actions that runspython scripts/validate_contracts.py contracts/on every push to contracts or models. A contract enforced at all three points cannot silently drift.
Contract-First Development Workflow
graph LR CONTRACT[Define Contract YAML] --> VALIDATE[Validate in CI] VALIDATE --> PRODUCER[Producer implements] VALIDATE --> CONSUMER[Consumer codes against contract] PRODUCER --> TEST[Contract tests in pipeline] TEST --> PUBLISH[Publish data]
- Producer and consumer agree on the contract (schema + SLA) before any code is written
- Contract is version-controlled alongside the pipeline code
- CI validates that produced data matches the contract
- Breaking changes require a new major version and migration period
Schema Definition Formats
| Format | Strengths | When to Use |
|---|---|---|
| JSON Schema | Human-readable, widely supported | REST APIs, config validation |
| Protocol Buffers | Strongly typed, backward-compatible by design | gRPC services, high-throughput |
| Avro | Schema evolution built-in, compact binary | Kafka/Pub/Sub messages |
| dbt YAML | Native to dbt, enforced at build time | Warehouse transforms |
| SQL DDL | Universal, everyone reads SQL | Database tables |
Data Contracts in Practice — Exported from Code
The contract examples above use YAML — a design-time format. The functional
pipeline takes a different approach: contracts are GENERATED from code at
runtime. Pydantic models define the schema, ColumnContext registries define
the semantics, and export_contracts() serializes both into a JSON Schema
file — a machine-readable contract that any consumer (including AI agents)
can parse.
See functional-pipeline-architecture for the architecture and 25_py_functional_pipeline for the implementation.
Example Contract: ESG Score Feed
Contract Header and Schema
The contract header identifies the dataset, owner, and version. The schema section defines every column with type, nullability, and business description.
# contracts/esg-scores-v2.yaml — header and metadata
contract:
name: esg-scores
version: 2.0.0
owner: esg-data-team
description: "Normalized ESG scores, 0-100 scale (higher = better)"# contracts/esg-scores-v2.yaml — schema definition
schema:
columns:
- name: instrument_isin
type: string
length: 12
nullable: false
description: "ISO 6166 ISIN identifier"
- name: vendor_code
type: string
nullable: false
enum: [MSCI, SUSTAINALYTICS, ISS, BLOOMBERG, CDP]
- name: normalized_score
type: decimal
precision: 5
scale: 2
nullable: true
constraints: { min: 0, max: 100 }
- name: score_date
type: date
nullable: false
- name: loaded_at
type: timestamp
nullable: false
primary_key: [instrument_isin, vendor_code, score_date]SLA and Change Classification
The SLA section defines freshness, availability, and quality thresholds. Breaking vs non-breaking changes follow semver: breaking = major version bump with migration period.
# contracts/esg-scores-v2.yaml — SLA and change rules
sla:
freshness: "Updated weekly by Monday 08:00 UTC"
availability: "99.9%"
quality:
completeness: ">= 95% of index universe covered"
null_rate: "< 2% for normalized_score"
breaking_changes:
- Removing a column
- Changing a column type
- Changing primary key
- Narrowing an enum
non_breaking_changes:
- Adding a new column (nullable)
- Widening an enum (adding new vendor)
- Relaxing a constraintExample Contract: Index Constituent Feed
Constituent Schema with SCD2-Style Dates
effective_date/expiry_datepattern enables point-in-time queries.weight_pctmust sum to 1.0 per index per date — a circuit-breaker quality check.
# contracts/index-constituents-v1.yaml — schema
contract:
name: index-constituents
version: 1.0.0
owner: index-operations-team
schema:
columns:
- { name: index_code, type: string, nullable: false }
- { name: instrument_isin, type: string, length: 12, nullable: false }
- { name: effective_date, type: date, nullable: false }
- { name: expiry_date, type: date, nullable: false, default: "9999-12-31" }
- { name: weight_pct, type: decimal, nullable: false, constraints: { min: 0, max: 1 } }
- { name: change_reason, type: string, nullable: true,
enum: [REBALANCE, IPO_ADD, MERGER_REMOVE, DELIST, SPIN_OFF_ADD] }
primary_key: [index_code, instrument_isin, effective_date]# contracts/index-constituents-v1.yaml — SLA
sla:
freshness: "Updated at quarterly rebalancing and on corporate actions"
quality:
weight_sum: "SUM(weight_pct) = 1.00000000 per index_code + date"
completeness: "Exactly N constituents where N = target count"Contract Testing in CI
Automated Contract Enforcement
Validates contract YAML syntax and runs dbt contract tests on every push that touches contracts or models. See github-actions-patterns for reusable workflow patterns.
# .github/workflows/contract-test.yml
name: Data Contract Validation
on:
push:
paths: ['contracts/**', 'models/**']
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install jsonschema pyyaml
- run: python scripts/validate_contracts.py contracts/
- run: dbt build --select tag:contract_test --target ciBreaking vs Non-Breaking Contract Changes
| Change | Breaking? | Action Required |
|---|---|---|
| Add nullable column | No | Minor version bump |
| Remove column | Yes | Major version, migration period |
| Change column type | Yes | Major version |
| Rename column | Yes | Major version |
| Add enum value | No | Minor version bump |
| Remove enum value | Yes | Major version |
| Tighten constraint | Yes | Major version |
| Relax constraint | No | Minor version bump |
| Change SLA | Depends | Communicate to all consumers |
Additive Changes Are Not Always Safe
“Adding a nullable column is non-breaking” is true for schema-aware consumers. But a consumer that does
SELECT *and feeds the result to a fixed-width parser, a strict Avro schema, or a Pydantic model withmodel_config = ConfigDict(extra='forbid')will BREAK on the new column. Additive changes are safe only when ALL consumers handle unknown fields gracefully. In practice, announce additive changes and give consumers a release window, even if they’re “non-breaking.”
Safe Pattern for Additive Changes
Before adding any column, audit downstream consumers for
SELECT *usage, strict Avro/Protobuf schemas, and Pydantic models withextra='forbid'. Publish the upcoming change in the contract YAML as a minor version bump with at least a two-sprint notice. Consumers should adoptmodel_config = ConfigDict(extra='ignore')for producer-owned schemas, and use explicit column lists (SELECT col1, col2) rather thanSELECT *so new columns are invisible until they opt in.
Producer and Consumer Responsibilities
| Responsibility | Producer | Consumer |
|---|---|---|
| Schema definition | Defines and maintains | Validates inputs against |
| SLA compliance | Monitors and guarantees | Monitors and alerts on breach |
| Breaking changes | Publishes new major version | Migrates within deprecation window |
| Quality checks | Validates before publishing | Validates after receiving |
| Documentation | Maintains contract YAML | References contract in their code |
| Incidents | Notifies consumers of issues | Reports anomalies to producer |
Data Contract Anti-Patterns
| Anti-Pattern | Problem | Better Approach |
|---|---|---|
| No contract exists | Schema changes break consumers silently | Define contracts before building |
| Contract not enforced | Contract exists but nobody checks | Automate validation in CI and pipeline (see data-quality-framework) |
| Verbal agreements | ”We agreed in a meeting” is not auditable | Version-controlled YAML contracts |
| Producer ignores consumer needs | Schema designed for producer convenience | Joint schema design sessions |
| No deprecation period | Old version removed immediately | Minimum 30-day deprecation window |
Related
- data-quality-framework — Quality gates that enforce contract SLAs at each medallion layer
- data-pipeline-testing-strategy — How contract tests fit in the data engineering testing pyramid
- serialization-formats — Schema formats (Protobuf, Avro, JSON Schema) and their evolution support
- dbt-transformation-layer — dbt model contracts with enforced schemas at build time
- streaming-architecture — Schema registries for event contracts in Pub/Sub and Kafka
- rest-api-design-and-consumption — API contracts parallel data contracts: versioning, backward compatibility
- error-handling-and-retry-patterns — What happens when contract validation fails: quarantine, DLQ, alerting