What Is a Data Contract and Do You Actually Need One?
A data contract is enforced in the producer's pipeline. Most teams asking for one actually want monitoring. How to tell which you need.
A data contract is a machine-readable agreement about the shape, meaning, and delivery guarantees of a dataset, enforced at the boundary between the team that produces it and the teams that consume it. The enforcement point is the entire idea. A check that runs after bad data has landed is a test. A check that stops bad data from being published is a contract.
Most teams that go looking for data contracts want something simpler and should build that instead. This post is about telling the two situations apart, because adopting contracts when you needed tests costs you months and produces YAML nobody enforces.
What problem is a contract actually solving?
One specific problem: somebody else can break your data and has no reason not to.
The canonical version. A backend engineer renames a column in the application database from user_type to account_type. Reasonable change, reviewed and approved by their team, ships on a Tuesday. Nothing in their world indicates that a downstream analytics pipeline reads that column, because nothing in their world mentions it exists.
Your ingestion picks up the new schema. Your staging model references user_type, which no longer exists, and fails. Or worse, it doesn't fail: the column still exists but now means something different, and your models produce numbers that are wrong and plausible.
Notice what would not have helped. A dbt test catches this at 02:00, downstream, hours after the change shipped and probably after the deploy window closed. That boundary is the subject of dbt tests vs data observability. Monitoring catches it slightly earlier and still after the fact. Both of them are telling you about damage.
A contract makes the producer's CI fail when they try to ship the rename. The change gets caught at 14:00 on Tuesday by the person making it, while they still have context, at the moment when fixing it is a two-line diff rather than an incident.
That's the whole value proposition, and it's genuinely large when it applies.
When is a contract the wrong answer?
Whenever you cannot make the producer's pipeline fail. Which is most of the time.
Run through your actual upstream sources:
| Where the data comes from | Can you enforce a contract? |
|---|---|
| Another internal team's service | Yes, if they agree |
| Your own application database | Yes, if the backend team is bought in |
| A SaaS vendor via Fivetran (Salesforce, Stripe) | No |
| A partner's API or file drop | Rarely, and only contractually |
| A CSV somebody uploads | No |
| Another company's data feed | No |
For every "No" row, a contract is documentation with extra steps. Those rows are exactly where schema change monitoring earns its keep instead. You can write the YAML, you can validate incoming data against it, and when it fails you are exactly where you were before: finding out after the fact, with nobody upstream obliged to care. That is monitoring wearing a contract's clothes, and calling it a contract creates a false sense of guarantee.
The honest test: can you make somebody else's build go red? If the answer is no, you want monitoring and you should skip to the end of this post.
This is why contracts get discussed far more than they get adopted. The technical part is easy and the organizational part is the whole job.
What does a broken interface actually cost?
Worth a number, because "contracts prevent incidents" is the kind of claim that never survives contact with a budget conversation.
Take the column rename. The producer ships it at 14:00. Your pipeline fails at 02:00. Somebody notices at 08:30, spends 15 minutes working out which change caused it, 30 minutes finding the right person on the producing team, and then waits for that person to have context on a change they made 18 hours ago and have since moved on from. Add a fix, a redeploy, and a backfill of one day of data.
Call it 2 hours of your time, 1 hour of theirs, and a day of a downstream dashboard being wrong. At a loaded cost of $80 to $150 an hour, the labour alone is roughly $300 an incident, and the wrong dashboard is the part with no price on it.
The contract version: their build fails at 14:05, they see the failure attached to their own pull request, and they fix it in the 10 minutes before they context-switch. Cost is close to zero because nothing downstream happened.
| Without contract | With contract | |
|---|---|---|
| When it is caught | 18 hours later | 5 minutes later |
| Who finds it | You | Them |
| Context available to the fixer | Gone | Full |
| Downstream data affected | 1 day, backfill needed | None |
| Rough labour cost | ~$300 | ~$20 |
Two honest caveats about that table. It assumes a breaking change that fails loudly; the silent kind, where a column keeps its name and changes meaning, is caught by neither unless the contract encodes semantics rather than just types. And a contract only pays out on interfaces that break. Applied to a stable interface, it is pure maintenance cost with no return, which is the argument for choosing your five interfaces carefully rather than covering 40.
The break-even is roughly this: if an interface has broken you twice in a year, a contract on it pays for itself. If it has never broken, you are buying insurance against a risk you have no evidence of.
What goes in one?
The Open Data Contract Standard is a Linux Foundation project defining a vendor-neutral YAML format. It has converged enough to be worth using rather than inventing your own shape.
apiVersion: v3.1.0
kind: DataContract
id: 3f9a1b7e-...
name: orders_contract
version: 1.2.0
status: active
schema:
- name: orders
physicalType: table
properties:
- name: order_id
logicalType: string
required: true
unique: true
- name: status
logicalType: string
quality:
- rule: validValues
mustBe: ['pending', 'shipped', 'delivered']
- name: order_total
logicalType: number
quality:
- rule: nullValues
mustBeLessThan: 1
slaProperties:
- property: frequency
value: 1
unit: hours
team:
- username: data-platform
role: owner
Four things, and each earns its place:
- Schema: names, types, nullability, uniqueness. The part that catches renames.
- Quality rules: valid value sets, null thresholds, row-count expectations. The part that catches meaning changes.
- SLA: how often it updates, how late is too late. The part that catches silence.
- Ownership: who to wake up. The part everybody skips and then regrets.
The ownership section is the one I would argue hardest for. A contract without a named owner is a document, and the first time it fails somebody has to work out whose problem it is, which is the thing you were trying to avoid.
Where do you enforce it?
Three enforcement points, and they are not alternatives. A serious setup uses all three, and most teams start with the middle one because it is the only one they control.
In the producer's CI. The strongest position. The producing team validates their output against the contract before merging. Their build fails, they fix it, nothing downstream ever sees it. Requires their cooperation, which is why it's rare.
At the ingestion boundary. You validate incoming data against the contract as it lands. Bad data is quarantined or alerted on before it reaches your models. You control this unilaterally, which is its entire appeal. It catches problems after they shipped but before they spread.
In your own pipeline. The contract becomes the source of truth for your monitoring config: the schema section generates schema-drift checks, the quality rules generate validity checks, the SLA generates a freshness monitor. This is the version that requires no cooperation from anyone.
That third one is worth dwelling on, because it changes what a contract is for. Instead of an agreement you enforce, it becomes a single declarative file your monitoring is generated from:
# Pull what is currently monitored, as a contract
armor contract pull --asset <uuid> -o orders.yaml
# Edit the freshness SLA, commit it, review it in a PR
vim orders.yaml
# Preview what would change before applying
armor contract plan --asset <uuid> -f orders.yaml
# Apply
armor contract apply --asset <uuid> -f orders.yaml
Config in Git, reviewed in pull requests, diffing cleanly so a reviewer sees which rule changed rather than a screenshot of a settings page. That works whether or not anyone upstream has ever heard of your contract, and it is most of the practical value for most teams.
Who is supposed to own the contract?
The question that sinks more contract programs than any technical detail, and the one nobody puts in the getting-started guide.
There are three plausible owners and each is wrong in a different way.
The producing team owns it. Sensible, since they are the ones enforcing it. Except they have no idea what consumers depend on, so the contract they write describes what they happen to emit rather than what anyone needs. When they want to change something, the contract is theirs to relax.
The consuming team owns it. Also sensible, since they know what they need. Except a contract written by the consumer and imposed on the producer is a requirements document, and it gets treated like one, which is to say it gets ignored until somebody escalates.
A central platform team owns it. Tidy on an org chart. In practice it puts a team with no stake in either side in the middle of every schema change, and they become a queue.
The arrangement I have seen work: the producer owns the contract, the consumer owns the tests against it. The producer commits to a shape and is responsible for not breaking it. The consumer verifies that the shape actually meets their needs and raises a change request when it doesn't. Both live in the producer's repo, so the person making a change sees both.
That only works if changing a contract has a defined path. Write it down before you need it: who proposes, who approves, how long consumers get to migrate, and what happens if they don't. A contract with no amendment process is a contract that gets bypassed the first time it is inconvenient, and after that it is decoration.
The version field exists for this. Bump the minor version for additive changes, the major for breaking ones, and give consumers a window on major bumps. That is not a data-specific idea, it is how every API has worked for twenty years, and data teams keep re-deriving it.
Do you need the full standard?
You need a format. Whether you need ODCS specifically depends on how much you care about the exit.
ODCS covers what tools agree on: schema, column quality rules, freshness SLAs, ownership, tags. It does not natively model everything a monitoring product does, so tools extend it through the spec's customProperties mechanism, which is what that mechanism is for.
The reason to prefer a standard over your own YAML is portability. A contract in a proprietary format is a config file that only one vendor reads, which puts you back where you started. That's also the reason we export in two modes: extended keeps every product-specific setting for round-tripping, and odcs-pure strips them so the file contains only what any ODCS tool can act on.
armor contract pull --asset <uuid> --mode odcs-pure -o orders.odcs.yaml
I'd hold that up as the test to apply to any tool making contract claims: ask for the export command and run it. A vendor that speaks a standard on the way in and not on the way out has sold you an import feature, not a contract.
How do you actually adopt one?
The failure mode is starting with the standard and a workshop. Start with one interface that has already hurt you.
- Pick the single worst producer relationship. The team whose changes have broken you twice. One contract, one interface, one consuming pipeline.
- Write the contract for what exists today, not what you wish existed. Generate it from the current table if you can. A contract that fails on day one because it describes an aspiration teaches everyone to ignore it.
- Enforce it at your ingestion boundary first. No negotiation required. You get value immediately and, more importantly, you get evidence.
- Take the evidence to the producing team. "This contract would have caught the last three incidents, here is the check, it adds 20 seconds to your build" is a conversation. "We should adopt data contracts" is a meeting.
- Only then move enforcement into their CI. With one working example, the second and third contracts are far easier than the first.
Expect the first one to take a few weeks, mostly in conversations rather than YAML. Expect the version number to matter sooner than you think, because the first time you need to change a contract you will discover whether you agreed on how changes get proposed.
What does this cost you?
Three costs, and the third is the one that kills adoption.
Writing it is a couple of hours per interface if you generate from the existing table, considerably more if you're negotiating semantics.
Enforcing it is compute, and it's modest: schema and quality validation on ingestion is cheap relative to the transformation work that follows.
Maintaining it is the real cost. Every legitimate schema change now requires a contract update, a review, and coordination between two teams. That is the point, it is the mechanism working, and it is also friction on changes that were fine. A team that adopts contracts across 40 interfaces and then finds every schema change needs a cross-team PR will quietly stop enforcing them within two quarters.
Which is the argument for a small number of contracts on the interfaces that actually break, rather than blanket coverage. Five contracts that are enforced beat 40 that are decorative.
Frequently asked questions
What is a data contract?
A machine-readable agreement about the schema, quality rules, and delivery guarantees of a dataset, enforced at the boundary between producer and consumers. The defining feature is the enforcement point: a contract stops bad data being published, rather than reporting it after it lands.
What is the difference between a data contract and a dbt test?
Enforcement point and owner. A dbt test runs in your pipeline after the data arrives and fails your build. A contract runs in the producer's pipeline before the data ships and fails theirs. Same assertions, very different economics.
Do I need data contracts?
Only if you can make the producing team's build fail. If your data comes from SaaS vendors, partner APIs, or file drops, there is no pipeline of theirs to fail, and you want monitoring instead.
What is ODCS?
The Open Data Contract Standard, a Linux Foundation project defining a vendor-neutral YAML format for data contracts. Using a standard rather than a proprietary format is what keeps the contract portable between tools.
Where should I enforce a contract?
Start at your own ingestion boundary, because it needs nobody's permission and produces evidence. Move enforcement into the producer's CI once you have a working example and a track record to point at.
Can a contract replace data quality monitoring?
No. A contract covers the interfaces you have agreements on, which is a subset of your warehouse. Everything else, including every table you get from a vendor, still needs monitoring.
How many data contracts should I have?
Few, and enforced. Five contracts on interfaces that have actually broken beat forty that nobody checks. Blanket coverage is how contract programs die, because every schema change starts requiring a cross-team pull request.
What happens when a contract needs to change?
You version it and the change gets reviewed like code. Agreeing on how changes are proposed and approved, before you need to make one, is the part teams skip and then regret at the worst moment.
Do data contracts prevent all data incidents?
No. They prevent breaking changes from producers who agreed to be bound. They do nothing about a vendor changing a currency unit, an ingestion job silently stopping, or anything upstream of the contract boundary.
Who should own a data contract?
The producing team owns the contract, since they enforce it, and the consuming team owns the tests verifying it meets their needs. Both live in the producer's repo so the person making a change sees both. A central team owning it turns them into a queue.
How do I handle changes to a contract?
Version it like an API: minor bump for additive changes, major for breaking ones, with a migration window for consumers on majors. Agree on who proposes and who approves before you need to, because a contract with no amendment process gets bypassed the first time it is inconvenient.
Can I generate a contract from an existing table?
Yes, and you should. Generating from the current state gives you a contract that passes on day one, which is what you want. A contract written from an aspiration fails immediately and trains everyone to ignore it.
What is the difference between odcs-pure and extended export?
extended includes tool-specific configuration under customProperties so the contract round-trips with every setting intact. odcs-pure strips those, leaving only the ODCS-native subset any compliant tool can read. Use pure when handing the file to another tool, extended for backup and version control.
How do I know a vendor really supports contracts?
Ask for the export command and run it. Plenty of tools import a standard format and emit only their own, which is an import feature rather than portability. If you cannot get your config out in a format something else reads, you do not have a contract, you have a settings page.