A payments team renames a field from amount_cents to amount_minor_units to support a new currency. Nobody outside the team notices until a downstream reporting job starts silently summing nulls. The dashboard looks fine. The numbers are wrong. Nobody finds out for three weeks, because nothing crashed, nothing alerted, and the field still existed, just under a name none of the consumers were reading.
That is the shape almost every integration outage takes. It is rarely a dramatic outage. It is a quiet drift between what a producer thinks it is sending and what a consumer thinks it is receiving, and the gap sits there until someone notices the numbers look off. A data contract exists to close that gap before it opens, and it's the kind of gap we run into constantly doing data integration and API work for clients with more than one team touching the same data.

Photo by K on Pexels
What a data contract actually is
A data contract is a written, versioned, machine-checkable definition of the shape a system promises to send or receive. Not a wiki page describing the API "as of last quarter." Not a comment in a Slack thread. An artifact that lives next to the code, gets reviewed in pull requests, and can be validated automatically against real payloads.
The distinction matters because documentation degrades and contracts don't, as long as something is actually enforcing them. A README can drift for a year before anyone reads it again. A schema that a CI job validates on every commit either holds or the build fails. That difference is the entire value proposition.
Most teams that skip this step aren't being careless. They genuinely believe the API surface is stable enough not to need it, right up until the first rename, the first added required field, or the first type change from string to integer breaks something three services away that nobody remembered depended on the old shape.
Start with the shape, not the prose
Before writing a single line of contract code, write down the actual shape of the data: field names, types, whether each field is required or optional, and what a null actually means for that field. This sounds obvious and gets skipped constantly, because most integration work starts from an example JSON payload someone pasted into a ticket rather than from a deliberate schema.
JSON Schema is the most common format for this, and it is worth adopting even if you never validate against it programmatically at first, because writing the schema forces the conversation that usually gets skipped: is this field ever going to be null, and does every consumer agree on what null means here versus an empty string versus a missing key.
For HTTP APIs specifically, OpenAPI gives you the schema plus the request and response shape plus the status codes, all in one document that both humans and tooling can read. It is not a replacement for a data contract so much as the natural home for one when the integration point is a REST endpoint rather than an event stream or a database.
Version the contract, not just the API
A version number on an API endpoint tells consumers almost nothing about what changed in the payload shape. /v2/orders might have the same fields as /v1/orders with one type changed, and that is exactly the kind of change a URL version number cannot express. The contract itself needs its own version, independent of the endpoint version, so a consumer can pin to "orders contract 3.1" and get an explicit answer about compatibility.
Semantic versioning gives a shared vocabulary for this: additive, backward-compatible changes bump the minor version, anything that could break an existing consumer bumps the major version. Adopting that discipline for a data contract, separate from whatever versioning scheme the API itself uses, is one of the cheapest changes a team can make and one of the highest-leverage ones.
The rule that actually prevents outages is simple to state and hard to enforce culturally: a major version bump requires a real migration path and a deprecation window, not a Slack message announcing the change is coming next sprint. Teams that treat contract versions as advisory rather than binding end up right back where they started.
Photo by Dimitri Karastelev on Unsplash
Enforce the contract in CI, not in a wiki page
A contract that only lives in documentation is a contract nobody actually checks against, which means it is really just documentation wearing a more official-sounding name. The step that turns a schema into an enforced contract is validating real request and response payloads against it automatically, on every deploy, before the change reaches anything downstream.
Pact and similar consumer-driven contract testing tools work by having the consumer specify exactly what it expects from a call, and then replaying that expectation against the real provider in CI. If the provider's response no longer matches what the consumer declared it needs, the build fails on the provider's side before the change ships, not three days later when a downstream job starts erroring in production.
This flips the usual failure mode. Instead of the consumer discovering a break after deployment, the producer discovers it before deployment, because the consumer's expectations are checked-in, versioned code rather than an assumption living in someone's memory of how the API used to work. It's the same reasoning behind most of the automated checks we build into a client's pipeline: catching a problem in a build log is cheap, catching it in a customer support ticket is not.

Photo by Tima Miroshnichenko on Pexels
Handling breaking changes on purpose
Not every change can be additive. Sometimes a field genuinely needs to be removed, renamed, or have its type changed, and pretending otherwise just delays the pain. The difference between a well-handled breaking change and an outage is almost never technical, it's whether the change was made deliberately with consumers in the loop or discovered by them after the fact.
A workable pattern: publish the new contract version alongside the old one, dual-write or dual-read during a deprecation window measured in weeks, and only retire the old shape once telemetry shows nothing is still consuming it. That telemetry step is the part teams skip most often, and it is the part that turns "we think nobody uses the old field" into "we can prove nobody uses the old field."
"The teams that get burned by API changes almost never lack the technical skill to version things properly. They lack a forcing function that makes silent breakage visible before a customer notices it first." - Dennis Traina, founder of 137Foundry
Contracts across event and queue boundaries
Everything above gets harder once the integration isn't a synchronous request and response but an event on a queue that might get consumed minutes, hours, or days after it was published. A consumer that comes back online after a deploy might be reading events that were written against an older contract version, and there is no request in flight to reject if the shapes don't match.

Photo by Djimmer Koster on Pexels
AsyncAPI extends the same contract-first thinking to event-driven systems, letting you document channel names, message schemas, and versioning for asynchronous integrations the same way OpenAPI documents synchronous ones. The mechanics differ, but the underlying discipline is identical: define the shape, version it explicitly, and validate against it before anything ships.
Include a schema version field directly in the event envelope itself, not just in an external registry. A consumer reading events from a backlog needs to know which contract version each individual message was written against, because the queue may contain a mix of versions during a rollout, and the consumer has to handle both without a shared assumption about which one is current.
Who owns the contract when three teams touch it
A contract with no clear owner tends to drift the fastest, because everyone assumes someone else is watching for breaking changes. The producing team usually needs to own the contract's evolution, but consumers need a real seat at the table before a breaking change ships, not a notification after the fact.
The simplest version of this that actually works: breaking changes require a pull request against the schema file itself, reviewed by someone from each consuming team, merged before the code change that implements it. That review step is what catches the case where a rename looks harmless to the producing team and catastrophic to a consumer relying on the old field name in a downstream mapping layer.
This is exactly the kind of coordination gap our data integration work tends to surface when we're brought in mid-project: not a lack of technical capability on either side, but no shared, enforced artifact that both teams are actually looking at. Building that artifact, and the automated checks around it, is usually a smaller lift than the outage it prevents.
What good looks like six months in
Teams that get this right eventually reach a point where a breaking API change is boring rather than alarming. The schema change shows up as a diff in a pull request, CI flags exactly which consumers declared an expectation the change would violate, and the rollout follows a deprecation window everyone already agreed to before the change was proposed.
That is a very different experience from the version most teams start with, where a breaking change is discovered by an on-call engineer staring at an unfamiliar error and working backward to figure out which upstream team changed something without telling anyone. The gap between those two experiences is almost entirely process, not tooling sophistication.
Getting started this week
Pick the single integration point that has broken silently most often, not the most complex one. Write down its actual current shape as a schema, even a rough one, and get it reviewed by someone on the consuming side. That first schema will surface disagreements about what a field means that nobody had explicitly discussed before, and that conversation alone prevents the next quiet drift.
From there, add one automated check that fails a build when the schema changes in a way the consumer didn't declare it could handle. You don't need every integration covered before this pays for itself. One enforced contract on the integration point that has burned you before is worth more than a documentation pass across everything you own.
If your team is weighing where a data contract fits alongside broader API or automation work, the honest answer is that it belongs wherever a silent schema drift has already cost someone a debugging afternoon. That is usually a shorter list than it feels like, and it is the right place to start.