Data Contracts 2026 — Why They Matter and How to Ship Them

Photo: SHVETS production / Pexels
A product engineer renames a database column from amount to amount_cents because the old name was ambiguous. The change is sensible, reviewed, and shipped on a Tuesday. On Wednesday morning the nightly pipeline copies the table into the warehouse, a revenue model that referenced the old name fails or, worse, quietly returns nulls, and a dashboard that finance reads at 9 a.m. shows a number that is wrong by an order of magnitude. A data contract is the discipline that makes that Tuesday change visible, reviewed by the people it affects, and blocked if it would break them.
This article explains what a data contract contains, how it differs from tests and monitoring, how the main tools enforce one, where the practice fails, and how a small team can adopt it without building a bureaucracy.
The failure that data contracts were invented to stop
Most data pipelines have the same shape. An application writes rows into an operational database. A pipeline copies those rows into a warehouse or lake. Analysts and engineers build models on top of the copy. Dashboards, reports, and machine-learning features read from the models. Every arrow in that chain is a dependency, and almost none of those dependencies is written down anywhere the producer can see.
The producer, usually a product engineering team, designs its tables for the application's needs. Nothing in their normal workflow tells them that a column is also the input to a revenue report, a churn model, and a regulatory filing. So when they rename, retype, or drop a column, the change passes code review because the application still works. The failure shows up later and somewhere else.
Think of it as a building where every apartment's plumbing runs through a neighbor's wall, and nobody has a map. Each tenant renovates freely within their own unit. Eventually someone cuts a pipe that feeds three other apartments, and the flooding is discovered in the basement a week later.
The pattern has a few recognizable stages:
- A source team changes a column, a type, a unit, or the meaning of a value.
- The pipeline propagates the change faithfully, because copying data is what pipelines do.
- Downstream models either fail loudly (the good outcome) or keep running on altered data (the bad one).
- Someone notices that a number looks off, often days or weeks later.
- A data engineer traces the problem backward through every hop to find the upstream change.
The expensive part is stage five. The investigation crosses team boundaries, so it needs meetings, and the fix usually needs a second change from the producer, who may not see the urgency. The silent cases are worse, because a decision may already have been made on the wrong figure. For the roles that typically own the downstream half of this chain, see our comparison of data engineering and data science.
What a data contract actually is
A data contract is an explicit, versioned agreement between the producer of a dataset and its consumers about what that dataset will look like and how it will behave. The important word is explicit. Every dataset already has an implicit contract, which is whatever downstream code happens to assume. A written contract moves those assumptions into a place where both sides can read, test, and negotiate them.
A useful contract covers five areas.
| Area | The question it answers | Example |
|---|---|---|
| Schema | What fields exist, with what types and nullability? | amount_cents is a non-null integer |
| Semantics | What does each field mean, including units and edge cases? | amount_cents is the charge in the smallest currency unit, before refunds |
| Service levels | How fresh, how often, and how reliably is it delivered? | Loaded within 60 minutes of the source event, 99% of days |
| Quality expectations | What is the acceptable shape of the values? | status is one of succeeded, failed, pending; no negative amounts |
| Change management | How are breaking changes announced and rolled out? | 30 days of notice and a parallel v2 table |
In practice a contract is three things bundled together: documentation that humans can read, automated checks that run against real data, and a review process for changes. Remove any one and it degrades. Documentation without checks is a wish. Checks without documentation tell you something broke but not what was promised. Both without a change process leave producers free to break things and apologize afterward.
Where the idea came from
The term gained traction in data engineering communities in the early 2020s, and Andrew Jones is one of the practitioners who wrote about it early. He later published the book Driving Data Quality with Data Contracts (Packt, June 2023). The concept borrows from older ideas in software: API contracts between services, schema definitions in remote procedure call systems, and consumer-driven contract testing in microservices. The new part was applying the same logic to datasets that flow through analytical systems, where the "API" is a table or a stream and the "client" may be a dashboard nobody on the producing team knows exists.
PayPal published a data contract template in 2023 describing the contract it used in its data mesh implementation, released under the Apache 2.0 license. That template has since evolved into the Open Data Contract Standard (ODCS), now maintained under Bitol, a project hosted by the Linux Foundation together with the AIDA User Group. Version 3.1.0 of the standard organizes a contract into sections including fundamentals, schema, data quality, service-level agreement, support channels, and team. The value of a shared standard is mostly tooling: if contracts are written in a common format, validators, catalogs, and code generators can read them.
Why "schema on read" did not remove the need
The data lake era of the 2010s popularized a different promise: store everything in raw form and decide the structure when you read it. That approach has real strengths for exploration and for data whose shape is unknown at ingestion. As the default for analytical pipelines that feed business reporting, it has aged poorly.
The reason is that structure does not disappear when it is deferred. Every consumer of a raw table ends up encoding a schema in its own code: this column is a timestamp, that one is an integer, this value is never null. Those assumptions live in dozens of queries, written by different people at different times, and none of them is checked against the others. When the source changes, each consumer fails or misbehaves in its own way.
An explicit contract does the opposite. It states the structure once, near the producer, and lets every consumer rely on the same statement. This is the same trade that statically typed languages make with type declarations: a little more writing up front in exchange for catching a class of errors before they reach production. Our overview of the modern data stack covers where schema discipline sits among the other layers.
What a contract looks like in code
Contracts are usually written in YAML or JSON and kept in version control next to the code that produces the data. The exact format depends on the tool. Two examples show the range.
A dbt source with freshness and tests
dbt, the transformation tool covered in our dbt tutorial, lets you declare sources (raw tables loaded by a pipeline), document them, set freshness expectations, and attach tests. This is not a full contract, because it is declared by the consuming side, but it captures the same elements.
sources:
- name: stripe_raw
config:
freshness:
warn_after: {count: 30, period: minute}
error_after: {count: 2, period: hour}
loaded_at_field: _loaded_at
tables:
- name: charges
description: "Stripe charge events. Owner: payments team."
columns:
- name: id
description: "Stripe charge ID"
data_tests:
- not_null
- unique
- name: amount_cents
description: "Charge amount in the smallest currency unit, as an integer"
data_tests:
- not_null
- name: status
data_tests:
- accepted_values:
arguments:
values: ['succeeded', 'failed', 'pending']
The freshness block follows the syntax in dbt's documentation: warn_after and error_after each take a count and a period, and loaded_at_field names the column dbt uses to find the newest row. Running dbt source freshness checks it. The four built-in generic tests in dbt are unique, not_null, accepted_values, and relationships. In recent dbt versions the YAML key is data_tests, with the older tests key still accepted as an alias.
A dbt model contract
Model contracts, introduced in dbt Core v1.5 on April 27, 2023 as part of the project's "model governance" features, work on the producing side. A model declares the exact columns, types, and constraints it promises to deliver, and dbt refuses to build it if the query does not match.
models:
- name: dim_customers
config:
contract:
enforced: true
columns:
- name: customer_id
data_type: int
constraints:
- type: not_null
- name: customer_name
data_type: string
With enforced: true, dbt runs a preflight check that the query returns columns matching the contract (column order does not matter), then includes the names, types, and constraints in the table creation statement so the warehouse can enforce them too. If the shapes disagree, the model fails to build instead of shipping a table that differs from what was promised.
Two limits apply. First, contracts apply to models, not to sources, seeds, or snapshots, and they support table, view, and incremental materializations but not Python models, materialized views, or ephemeral models. Second, constraint enforcement depends on the warehouse. dbt's documentation sorts constraints into three groups: definable and enforced, definable but not enforced (metadata only), and not definable at all. In general not_null is the constraint most platforms enforce, while primary_key, foreign_key, and unique are often recorded without being checked. A constraint the platform does not enforce is not part of the contract in any meaningful sense.
What dbt treats as a breaking change
dbt also compares a project against a previous state and raises a contract error for changes that could break downstream consumers. Removing an existing column, changing a column's data type, and removing or changing its constraints (from v1.6) count as breaking. From v1.9, deleting, renaming, or disabling a contracted model also counts. dbt's guidance is to treat a breaking contract change like a breaking API change and publish it as a new major version of the model, which lets consumers move on their own schedule.
This is a good illustration of what a contract adds beyond a test. A data test checks the content of a table after it is built. A contract checks the shape before the table is built and, with state comparison, checks whether a proposed change would break people who depend on it.
Streaming data and schema registries
Batch pipelines have a convenient property: if a table is wrong, you can rebuild it. Streams are less forgiving, because a message written with the wrong shape may already have been consumed by a dozen services. The streaming world therefore adopted contracts early, in the form of schema registries.
A schema registry is a service that stores the schema for each topic and checks every new version against rules before it is accepted. Confluent Schema Registry, the best-known example for Apache Kafka, supports Avro, Protobuf, and JSON Schema. Its central idea is the compatibility mode.
| Mode | What the new schema is checked against | Who is protected |
|---|---|---|
| BACKWARD (default) | The latest registered version | New consumers can read data written with the latest and the previous version |
| BACKWARD_TRANSITIVE | Every registered version | New consumers can read all history |
| FORWARD | The latest registered version | Existing consumers can read data written with the new schema |
| FORWARD_TRANSITIVE | Every registered version | Consumers on any version can read new data |
| FULL | The latest registered version | Both directions, against the latest version |
| FULL_TRANSITIVE | Every registered version | Both directions, against every version |
| NONE | Nothing | Nobody; checks are disabled |
The practical effect for Avro is easy to state. Adding an optional field, meaning one with a default value, is compatible in every mode. Adding a required field with no default breaks backward compatibility, because a new reader would have nothing to fill in when it reads old records. Removing a required field with no default breaks forward compatibility for the mirror-image reason. The registry rejects an incompatible schema at registration time, so the producer finds out in its own build instead of the consumer finding out in production.
The registry enforces schema, which is only one of the five contract areas. Meaning ("is this field in dollars or cents?"), freshness, and volume are still outside what a compatibility check can see.
Contracts, tests, and observability are different things
Teams new to the topic often ask whether a contract is just a bundle of data tests, or whether a data observability tool already does the job. They overlap, but each answers a different question.
| Practice | Main question | When it acts | Who defines expectations |
|---|---|---|---|
| Data contract | What was promised, and by whom? | Before and during delivery | Producer, with consumer input |
| Data test | Does this table satisfy a specific rule right now? | After a build or load | Whoever writes the test |
| Data observability | Did anything unusual happen? | Continuously, after the fact | Learned from history or set by rule |
| Data lineage | Who depends on this? | On demand | Derived from code and queries |
A contract sets the expectation. Tests verify parts of it. Observability catches deviations the contract did not anticipate, such as a gradual drift in a distribution. Lineage tells you whom to notify when a contract has to change, which is why it pairs naturally with contract work; see our piece on data lineage and our guide to data quality monitoring for those halves.
A short example shows the division of labor. Suppose amount_cents is promised to be a non-negative integer.
- The contract states that promise and names the payments team as owner.
- A test checks that no row in last night's load has a negative value.
- An observability tool notices that the average amount dropped 40% overnight even though every row is a valid non-negative integer, which suggests a currency or unit change that no rule described.
- Lineage shows that the finance dashboard and the fraud model both read the column, so those owners are told before the next change.
None of the four makes the others redundant.
Who owns what: the producer and consumer conversation
The hardest part of data contracts is not YAML. It is that a contract only works if the team that produces the data agrees to be bound by it, and that team usually has other priorities.
What producers need to accept
A producing team that adopts contracts commits to a few things. It treats datasets that others depend on as products with interfaces, not as private implementation details. It announces schema changes before they ship, through a pull request or a review step, instead of after. It treats a breaking change as breaking, which means versioning or migration support. And it carries some of the operational burden, such as being paged when the contract is violated at the source.
That last point is where many initiatives stall. If the contract is owned by the data team and the producers see it as someone else's problem, checks fail, tickets pile up, and the producers learn to ignore them.
What consumers need to provide
Consumers have obligations too. They should state what they actually rely on, which is usually a handful of columns and not the whole table. Asking for "everything, forever" produces a contract so wide that no one can change anything. Consumers should also build in tolerance for changes the contract allows, such as new columns appearing, instead of writing SELECT * into fragile code.
Making the arrangement durable
Organizations where this works tend to share traits: named owners for each dataset, a shared on-call arrangement for pipeline incidents, and leadership that treats data reliability as part of engineering quality. The tooling reflects the agreement but does not create it. A team that installs a validator and never talks to its producers has bought a smoke detector for a house whose residents do not answer the alarm.
Patterns that tend to work
Several patterns recur in teams that report success with contracts. None is a guarantee, and which one fits depends on size and architecture.
A boundary between raw and trusted data. Raw data lands in one schema with minimal guarantees. A second layer, often called "bronze" or "staging" in layered warehouse designs, applies the contract and tests. Everything downstream reads from the trusted layer, so a bad upstream change is caught at the boundary and does not spread.
Contract review in pull requests. Schema files live in version control. A change to a contract file triggers automated checks and requires approval from named consumer representatives, in the same way a change to a public API would.
Versioned contracts. Breaking changes ship as a new version, with the old one kept alive for an announced period. Consumers migrate when ready, and the producer retires the old version on a known date. Teams that borrow API versioning habits find them transfer well.
Producer-owned tests. The team that creates the data writes and maintains the checks that guard it. Because the producer understands the source system, the checks tend to be better targeted, and the producer feels the failures first.
Contract violations as service-level objectives. Freshness and validity are tracked like uptime. Repeated violations trigger a review, which keeps contracts from decaying into documentation.
Code generation from the contract. When the contract is machine-readable, producers can generate typed classes, database migrations, or documentation from it. This reduces the chance that the contract and the code drift apart, because one is derived from the other.
Where data contracts fail
The practice has failure modes, and they are mostly organizational.
- Contracts without enforcement. A contract that is never executed against real data is documentation. Checks must run on every load or every build, and failures must have consequences.
- Heavyweight approval. If every column addition needs a committee, teams route around the system by writing to unofficial tables. Additive, non-breaking changes should be fast and mostly automatic.
- Ownership that sits only with the data team. Without producer ownership, a contract binds nobody who can change the source.
- No definition of "breaking." Without an agreed list (dropping a column, changing a type, changing units, tightening nullability), every change becomes a negotiation.
- Wrong granularity. Contracts only at the table level miss column changes. Contracts on every field become unmaintainable. A workable middle is to contract the table and its key fields: identifiers, amounts, timestamps, and status values.
- Semantic drift. A schema check cannot tell that
revenueswitched from dollars to cents. Types match and the data is wrong. Semantics need written descriptions and, where possible, range checks that would catch an order-of-magnitude shift. - Treating the tool as the practice. Installing a validator is a day's work. Agreeing who owns what, and who is paged, is the actual project.
Common misconceptions
Several things resemble data contracts without being them.
Documentation alone. A wiki page describing a table is useful, but without automated checks it will diverge from reality within months.
A schema file in the repository. Necessary, not sufficient. If nothing validates production data against the file, it describes an intention.
Strong typing in the warehouse. Types stop some mistakes, but they cannot detect that a value changed meaning. A column of integers stays a column of integers when the unit changes.
Monitoring alone. Detection tells you something is off, but without an agreed expectation you cannot say who is wrong, the producer or the consumer.
A contract equals data mesh. Data mesh, the organizational approach that treats datasets as products owned by domain teams, makes contracts central, which is why PayPal's template came out of a mesh effort. But contracts are useful in a conventional centralized warehouse too.
A rollout plan for a team starting from zero
A realistic first quarter looks like this. The timings are rules of thumb, not measured benchmarks, and a small team can compress them.
Weeks 1 to 4: find the load-bearing tables. List the datasets that many things depend on: customers, payments, core product events. Use lineage tools, or ask analysts which tables they would panic about if they broke. Write a one-sentence description for each column of those tables. Name a producing team for each.
Weeks 5 to 8: write the checks. Start with the cheap and valuable ones: not-null on identifiers, uniqueness on primary keys, accepted values on status columns, freshness on the loads. Run them inside the pipeline you already have, whether that is dbt or an orchestrator. Our comparison of Airflow, Prefect, and Dagster covers where such checks can sit in the run.
Weeks 9 to 12: get agreement. Present each contract to its producing team and get explicit sign-off. Agree on what counts as breaking, how much notice consumers get, and who is contacted when a check fails. Move contract files into version control with required reviewers.
After that: expand deliberately. Add contracts when new important tables appear. Where the pipeline is on a distributed engine, apply the same checks at the point where data lands; our primer on Apache Spark explains what that layer does. Review violations monthly: which contracts fail most, and is that a sign of a bad producer process or a bad contract?
Starting small matters. A team that contracts ten tables and enforces them will learn more than a team that drafts a company-wide standard and never runs it.
How contracts reach beyond analytics
The same logic extends to machine learning. A model trained on a feature that silently changes meaning will degrade without any code change; the topic is covered in our explainer on model drift in production. Contracts on the inputs, together with a feature store that serves one definition of each feature, reduce the gap between what a model was trained on and what it sees in production. For deployed AI systems, observability tooling addresses the monitoring half of the same problem.
Regulation is another driver. Where an organization must show how a reported figure was produced, a versioned contract, with its owner and change history, is a cleaner audit trail than a conversation reconstructed from chat logs. That is a general observation about auditability, not legal advice, and requirements vary by jurisdiction and industry.
Frequently Asked Questions
Q: Do small teams really need data contracts? A: Usually a light version, yes. Even a team of three can have incidents where someone assumed a column meant something it did not. Descriptions plus a handful of automated tests on the tables everyone uses capture most of the benefit without a formal process.
Q: Who owns the contract, the producer or the consumer? A: The producer owns the contract and is responsible for honoring it, with consumers contributing requirements and signing off on changes. Consumers own documenting what they depend on. If only the data team owns it, the people who can break the data are not bound by it.
Q: How do contracts differ for streaming and batch data? A: Streaming typically enforces the schema at write time through a schema registry, such as Confluent Schema Registry, with compatibility modes like BACKWARD and FULL. Batch typically enforces it in the warehouse or transformation layer, for example with dbt model contracts and tests. The goals are the same; the enforcement point differs.
Q: What is the difference between a data contract and a dbt test? A: A dbt model contract checks the shape of a model's output (columns, types, constraints) before the table is built, and dbt can flag changes that would break consumers. A data test checks the content of a table after it is built, such as uniqueness or accepted values. Many teams use both.
Q: How do contracts relate to data mesh? A: Data mesh treats datasets as products owned by domain teams, and products need stated interfaces. A data contract is that interface. PayPal's published contract template, which became the Open Data Contract Standard, was written for its data mesh work, though contracts are useful outside a mesh.
Q: How should a contract handle products that change quickly? A: Version it. Additive changes, such as a new optional column, can ship under the current version. Breaking changes ship as a new version while the old one stays available for an announced period, so consumers can migrate on their own schedule. This mirrors how public APIs are versioned.
Q: Where can I learn the details? A: The dbt documentation on model contracts and versions covers enforcement in detail, the Confluent documentation covers schema compatibility, and the Open Data Contract Standard repository under Bitol documents a vendor-neutral format. Andrew Jones's book, Driving Data Quality with Data Contracts, is a book-length introduction.
The takeaway
Data contracts are less a technology than a change in who is allowed to surprise whom. The tools, whether dbt model contracts, a schema registry, or a standard like ODCS, make a promise checkable. The practice only holds if producers accept that others depend on their tables and agree to announce and version their changes. Start with the handful of datasets that would hurt most if they broke, write down what they promise, run the checks every time, and put a named owner on each. Everything beyond that is refinement.
Sources
- dbt Labs, "Model contracts" documentation: docs.getdbt.com/docs/mesh/govern/model-contracts
- dbt Labs, "dbt Core v1.5: model governance of, by, & for people": getdbt.com/blog/dbt-core-v1.5
- dbt Labs, "Source freshness" reference: docs.getdbt.com/reference/resource-properties/freshness
- dbt Labs, "Data tests" documentation: docs.getdbt.com/docs/build/data-tests
- Confluent, "Schema Evolution and Compatibility" for Schema Registry: docs.confluent.io
- Bitol, Open Data Contract Standard documentation: bitol-io.github.io/open-data-contract-standard
- PayPal, data contract template repository (now ODCS): github.com/paypal/data-contract-template
- Jones A, Driving Data Quality with Data Contracts, Packt Publishing, 2023: oreilly.com/library/view/driving-data-quality/9781837635009
Read next:
Follow new articles
Add SciHub101 to your feed reader to see new science and technology stories.
About the Author
SciHub101 is an independent science and technology publication founded and edited by Buğra Sözeri, a software engineer and app developer based in Türkiye. AI tools assist with drafting and research. Source documentation in the existing archive is uneven, and articles are being reviewed and corrected. The Editorial Team byline identifies editorial responsibility; it does not certify specialist or independent human review. Where we compare products, anything built by our own team is labelled as such — see the editorial guidelines and About page for how corrections and disclosures work.
Related Articles

Feature Stores Explained — Feast, Tecton, SageMaker
Feature stores for ML: what they are, why they matter, when to use Feast vs Tecton vs AWS SageMaker. Solve the training/serving skew problem.

Lakehouse Formats 2026 — Iceberg vs Delta Lake vs Hudi
Iceberg vs Delta Lake vs Hudi in 2026: how each lakehouse table format works, where each wins, how catalogs and migration fit, and how to choose.

Apache Spark Explained — Big Data Processing 2026
How Apache Spark works: RDDs, lazy DAG execution, drivers and executors, Spark SQL, AQE, streaming, and when single-node tools like DuckDB or Polars fit better.

Data Engineering vs Data Science — Which Career Path 2026
Data engineering vs data science: what each role does, the skills and tools they need, how the work differs, and how to choose or switch between them.

dbt Tutorial — Data Engineering for Analysts (2026 Guide)
dbt (data build tool) explained for data analysts: SQL-based transformations, models, tests, documentation. The modern way to build data pipelines.
