Skip to content

Data contracts

A data contract is a written, machine-readable agreement between the producer of a dataset and its consumers about its structure, meaning, quality and service levels, checked automatically.

Before reading

Governance roles · dbt

Why it matters

Consumers build on data based on assumptions: this column is never null, revenue excludes tax, the table is ready by six. Producers change their systems without knowing which assumptions they break. A contract writes the assumptions down, makes the producer responsible for keeping them, and lets tooling catch a breaking change before it reaches anyone.

How it works

A contract covers several kinds of promise.

Schema: the tables, columns, types, keys and constraints.

Semantics: what each field means. amount is in euros including tax; a cancelled order has status cancelled and is never deleted. Changes in meaning are the hardest to catch, because no schema check sees them.

Quality: rules the data meets, such as uniqueness of order_id, the share of rows allowed to have a missing value, or a reconciliation with the source.

Service levels: when data is available (freshness), how often it is updated, how long it is kept, and what support consumers can expect when something breaks.

Ownership and terms: who owns it, who to contact, which uses are allowed, how sensitive it is, and how changes are announced. A versioning rule is part of this: compatible changes produce a minor version; breaking changes produce a new major version, with notice and a period in which both are available.

Contracts are files, usually YAML, kept in version control next to the code that produces the data. The Open Data Contract Standard (ODCS), maintained by the Bitol project under the Linux Foundation, is a widely adopted open format; its current version is 3.2 (checked October 2026). Tools such as datacontract-cli can lint a contract, test real data against it, and compare two versions to find breaking changes. Contract-like checks also exist inside tools: dbt model contracts, for example, enforce column names and types when a model is built.

flowchart LR
  PR[Producer changes<br/>model or contract] --> CI{CI checks}
  CI -->|schema matches contract?| A[Build in test<br/>environment]
  CI -->|breaking change<br/>without new major version?| X[Blocked]
  A --> Q{Data meets quality<br/>and freshness rules?}
  Q -->|yes| P[Published]
  Q -->|no| X2[Not published,<br/>owner alerted]
  P --> C[Consumers rely on<br/>the contract version]

Enforcement happens in two places. In continuous integration, a change to a model is checked against its contract: if the output schema no longer matches, or the contract itself changed in a breaking way without a new major version, the change is blocked. In production, each run tests the data against the contract's quality rules and service levels, and failures stop publishing and alert the owner.

Contracts belong where data crosses a boundary: published data products, gold tables used by other teams, and events from applications to the platform. Internal intermediate tables usually do not need one.

Key terms

  • Data contract: a machine-readable agreement on a dataset's schema, meaning, quality and service levels.
  • Producer: the team that creates and changes the dataset.
  • Consumer: anyone who relies on the dataset.
  • Service level: a promise about availability, such as data ready by 07:00.
  • Breaking change: a change that can break consumers, requiring a new major version.
  • ODCS: the Open Data Contract Standard, a YAML format for contracts.

Example

A contract for fct_daily_sales, abbreviated, in ODCS:

apiVersion: v3.2.0
kind: DataContract
id: shop-fct-daily-sales
name: Daily sales
version: 2.1.0
status: active
description:
  purpose: Revenue per day and product for finance and marketing reporting.
  usage: Revenue excludes cancelled orders and includes tax, in EUR.
schema:
  - name: fct_daily_sales
    properties:
      - name: sales_date
        logicalType: date
        required: true
      - name: product_id
        logicalType: string
        required: true
      - name: revenue
        logicalType: number
        description: Sum of order line amounts, tax included, cancelled orders excluded.
    quality:
      - type: sql
        description: One row per day and product.
        query: SELECT count(*) FROM (SELECT sales_date, product_id FROM shop.fct_daily_sales GROUP BY 1, 2 HAVING count(*) > 1)
        mustBe: 0
slaProperties:
  - property: timeOfAvailability
    value: 07:00 Europe/Berlin
    element: fct_daily_sales.sales_date
team:
  name: orders-data

If the orders team changes revenue to exclude tax, the column keeps its name and type, so no schema check fails. The contract's usage text is what makes this a breaking change, which requires version 3.0.0 and notice to finance.

Pitfalls

  • Contracts nobody checks. A YAML file without CI and runtime checks is documentation that drifts.
  • Only schema. Meaning and service levels are where most breakage happens.
  • Contracts on everything. Internal tables change often; contract the published interfaces.
  • Consumers left out. A contract written by the producer alone reflects what is easy to promise, not what is needed.
  • No versioning rule. Without one, every change is argued case by case.

Learn more

  • Bitol, Open Data Contract Standard: https://bitol-io.github.io/open-data-contract-standard/
  • Data Contract CLI documentation: https://cli.datacontract.com/
  • dbt documentation, model contracts: https://docs.getdbt.com/docs/mesh/govern/model-contracts

Schema evolution · Data quality dimensions · Governance as code · Data products and data mesh · Environments and CI