Catching a Breaking API Change Before It Ships
Integration bugs between two services are some of the most expensive to find late, because each service's own tests pass fine on their own, and the mismatch only shows up when they actually talk to each other in production. Contract testing exists specifically to catch that gap earlier.
Here's how it actually works and where teams get the setup wrong.
Vendors Covered in this Article
Disclosure: We may earn a commission if you buy through some links on this page. It doesn't change what we recommend.
What a contract test actually checks that a unit test doesn't
A unit test checks that a service behaves correctly in isolation. A contract test checks that a service's actual request and response shapes still match what the other side expects, without needing both services running together in a shared environment. It catches exactly the kind of change, a renamed field, a type that changed from a string to a number, a field that quietly became optional, that unit tests on either side would miss entirely.
Think of it as a test of the agreement between two services, rather than a test of either service's internal logic. It matters most exactly where independent teams own each side of an integration, since neither team can see the other's own test suite directly, and a contract test becomes the fastest shared signal that the agreement between them has quietly drifted.
Consumer-driven contracts: letting the consumer define what it needs
In a consumer-driven approach, the service consuming an API writes down exactly what it actually uses from the response, not every field the provider happens to return. The provider then runs that consumer's expectations against its own real implementation before shipping a change.
This matters because it stops a provider from being blocked by a consumer's use of a field the consumer doesn't actually care about, while still catching changes that would genuinely break something a consumer depends on.
Where teams get the setup wrong
The most common mistake is writing contract tests that check the entire response shape field by field, which makes the test brittle and prone to failing on harmless additions, like a new optional field the consumer never reads. Write the test to check only what the consumer actually depends on, so it fails for changes that matter and stays quiet for ones that don't.
The second common mistake is running contract tests only occasionally, or only manually before a big release, rather than as a standard part of every relevant pull request, which defeats the point of catching the break early.
Building this into a review process people will actually follow
Contract testing works best paired with a lightweight, written process: whenever a service's request or response shape changes, its contract tests run automatically against every known consumer before the change can merge. Document that process clearly enough that a new engineer can follow it without asking, since a process that lives only in one person's head disappears the moment that person is out.
Keep the process itself short. A one-page checklist that engineers actually read beats a longer document that becomes something people skip past. Review it with both teams a few weeks after adopting it, and cut anything that turned out to be more ceremony than protection.
A worked example: a field that quietly changed shape
Say a service changes a customer identifier field from a numeric type to a string, a reasonable change on its own. Without a contract test, this ships cleanly because the service's own tests never check how a consumer parses that field. A consumer relying on the old numeric type breaks in production the next time it tries to use that value, and the failure shows up far from the actual change, which makes it slower to trace back to the real cause.
Deciding which integrations are worth this investment
Contract testing has real setup cost, so it's worth prioritizing for integrations where a break is expensive: anything customer-facing, anything involving money, or any integration that's broken before without anyone catching it until a customer reported it. A rarely-changing internal integration between two services owned by the same small team, where a break is quick to notice and fix directly, often doesn't need the same level of investment.
Start contract testing where it pays off, and keep the tests lean:
- Prioritize integrations where a break is expensive, especially anything customer-facing or tied to revenue, before spending effort on low-risk internal calls.
- Add contract tests for integrations that have slipped through to production before, since they have already shown they can break unnoticed.
- Let each consumer record only the fields it actually uses, so the test is not brittle and does not fail on harmless changes.
- Run the contract check before merging, so whoever changes a request or response shape sees a break at the point of change.
- Treat an intentional breaking change as a coordinated conversation between both teams, not a reason to disable the failing test.
What Good Looks Like
Good here means a change to a service's request or response shape that would break a real consumer gets caught automatically before it merges, not discovered in production after a consumer's request starts failing.
Building The Capability (5-Stage Skill Ladder)
How to Get Started
Disclosure: We may earn a commission if you buy through some links on this page. It doesn't change what we recommend.
Frequently Asked Questions
Is contract testing the same thing as integration testing?
No. Integration testing runs real services together to check they work correctly as a whole, which is slower and needs a full environment. Contract testing checks the shape of the agreement between services without needing both running together, which makes it faster and easier to run on every relevant change.
Do we need contract tests for every internal service?
Not necessarily. Prioritize integrations where a break is expensive or has slipped through before, especially anything customer-facing or tied to revenue. A low-stakes internal integration between two services owned by the same team can often rely on quick manual detection instead.
What happens when a contract test fails, who fixes it?
Whoever is making the change that broke the contract, ideally caught before merging. If the change is intentional and the consumer genuinely needs to update, that's a coordinated conversation between both teams, not something to push through by disabling the test.
About the numbers
This guide doesn't quote a sourced benchmark. Figures in it are estimates or general guidance, so check them against your own numbers.
Related Guides
How to Build an Evaluation Framework You'll Actually Trust
How to build a continuous evaluation framework that reliably catches quality regressions, instead of a single score nobody fully believes in.
Stress-Testing a System Without Taking Down Real Traffic
How to run a synthetic load test that finds where a system actually breaks, without accidentally taking down production traffic in the process.
How to Ship a Risky Change Without a 2am Rollback
A concrete walkthrough of how to plan a risky production deployment: how to split it, what to watch, and when to decide the rollback trigger.
How Ephemeral Test Environments Actually Pay for Themselves
Where on-demand, per-branch test environments actually save money and reviewer time over shared staging, and the setup mistakes that erase those savings.
Build or Buy for Verifying Every Device That Connects?
How to split device identity from device posture checking, what building either one in house actually costs, and where a platform earns its keep instead.
Diagnosing Slow Requests Before You Blame the Database
A step-by-step way to find out whether a slowdown is the network, the app, or the database, before you add caching or upgrade infrastructure to fix it.