Diagram showing a CONSUMER service and a PROVIDER service — what the consumer assumes the provider returns versus what it actually returns after a change, with a broken connection representing the invisible gap, and a contract document closing that gap in the lower half.
Testing, Engineering Practices, Microservices

Your Services Talk to Each Other. Nobody Wrote Down What They Agreed To.

By Shivani Sutreja7 min read

When two services integrate, an implicit agreement is formed alongside the code — a field name, a response shape, an error code — that was never written as a test. Both services keep shipping. The agreement does not update. Contract testing makes that assumption explicit: the consumer defines what it needs, the provider proves in CI that it still delivers that, and breaking changes are caught before they reach production.

When two services integrate, something invisible is created alongside the code: an agreement. A field name. A response shape. The consumer knows what to expect because someone read the provider's API, wrote code to match it, and moved on. That expectation was never written as a test. It lives in the developer who built the integration, in an old PR description, in a Slack thread nobody reads anymore.

Both services keep shipping. The agreement does not update.

The Agreement No Test Is Watching

Every microservice boundary is an assumption. The consumer assumes the provider still returns that field name. The provider does not know who its consumers are or what they rely on. Neither side is negligent. The agreement was never explicit enough to break cleanly.

When the provider changes — a field renamed, a required header added — the consumer's tests do not catch it. The consumer's tests run against a mock: a snapshot of what the provider looked like when the mock was written. The mock does not update when the real provider does. Both sides report green. The incompatibility surfaces in production, on a code path nobody was watching.

This is why integration tests cannot catch API drift. They test against assumptions, not against reality.

What It Looks Like

An account service returns this when a consumer queries a user:

{ "userId": "u_123", "status": "active" }

Six months later the account team renames fields for consistency with a new internal standard. Their service now returns:

{ "id": "u_123", "accountStatus": "active" }

Their tests pass — they updated their own tests. The consumer's mock still has userId and status. The consumer's integration tests pass — they are running against the old mock. CI is green on both sides.

The first time the real provider and the real consumer talk to each other is in production, when the consumer tries to read userId from a response that no longer has that field.

What Contract Testing Is

A contract test is a written record of an assumption, attached to the code that makes it, verified by the code responsible for meeting it.

The consumer writes a test that defines the specific interaction it depends on: given this request, I expect this response shape. Running that test produces a contract file — a machine-readable record of the assumption — and publishes it to a shared registry.

The provider, on every commit, pulls every registered consumer contract and verifies the real implementation satisfies each one. If the provider's change breaks a consumer's assumption, the provider's CI fails before the change merges.

In the example above: the consumer's contract asserts it expects userId and status. When the account team renames those fields, the consumer's verification fails in the account team's CI pipeline — the rename is caught before it ships, not when the pager fires.

Flow diagram showing the contract testing loop — consumer writes contract test which generates a contract file, file is published to a registry, provider CI pulls all registered contracts and verifies, catching breaking changes before they merge.
Figure 1: The consumer writes the contract. The registry stores it. The provider verifies it in CI before any change merges.

Consumer-driven contract testing tools — Pact is the most widely adopted, with support across most language stacks — follow this model. The specific tool matters less than the discipline: the assumption lives in a file, both sides verify against it, and the registry is the source of truth for every active agreement in the system.

What Changes for the Provider Team

The consumer benefit is obvious: catch breaks before production. The provider benefit is less discussed, and it is where contract testing gets genuinely useful.

When consumer contracts are registered, the provider team gains visibility they did not have before. They can now see which consumers exist, what each one depends on, and — before a change merges — whether that change would break any of them.

That changes the relationship to API evolution entirely. Without contracts, a provider team making a breaking change has to rely on manual coordination: send a Slack message, hope consumers respond, wait for the integration environment to surface failures. With contracts registered in CI, the answer is immediate — here is every consumer that would break on this change, here is what they depend on, here are the teams who own them.

The pattern that formalizes this is can-i-deploy: before merging, the CI check answers whether this version of the provider is compatible with all registered consumer versions currently in production. The answer is binary. Either every registered consumer's contract is satisfied — ship with confidence — or specific contracts fail, identifying exactly which teams need to be involved before the change can land.

This is the shift from defensive to enabling. Without contracts, the provider makes changes on faith. With contracts, they make changes with a verified dependency map. They can confidently rename a field and know which consumers read it. They can deprecate a response property and check whether any registered consumer actually uses it before removing it. They can add a required field and see the full impact before the PR merges.

The agreement was never just for catching failures. Written down and verified in CI, it becomes the communication protocol between teams that cannot constantly coordinate — and the governance layer that makes confident independent deployment possible.

Where to Start

Find the integration where a broken assumption would cause the most damage. The order service calling the payment service. The auth service the gateway trusts.

Write one contract test for one interaction on the consumer side. Publish the contract. Wire the provider verification into its CI step. That test tells you something no amount of mocking tells you: whether what you assume the provider does today is what the provider actually does.

If it passes, you have a baseline. If it fails, you found a drift that was invisible in every other layer of the test suite.

Add interactions as new dependencies appear. The registry accumulates a versioned record of every agreement in the system. The implicit contract graph — which has always existed — becomes explicit, one assumption at a time.

The Bottom Line

Service boundaries are where teams meet. When teams are independent — different schedules, different ownership, different deployment cadences — those boundaries need an explicit governance layer. Not documentation that drifts. Not verbal agreements that fade. Tests that both sides run in CI.

The contract registry is that governance layer. It turns the implicit contract graph of a distributed system into something machine-verifiable: every consumer's expectations, every provider's obligations, and a CI gate that enforces the match before code ships.

The agreement was always there. Writing it down is what lets it break cleanly — in a failing build, before merge — instead of invisibly, in production, after the fact.

Frequently Asked Questions

What is contract testing and how is it different from integration testing?

Integration tests verify a service's behavior against mocked dependencies — snapshots of what those dependencies looked like when the mocks were written. When the real dependency changes, the mock stays frozen. The test passes against an assumption that no longer holds. Contract tests verify that a consumer's expectation of a provider matches what the provider actually delivers today. Integration tests catch internal inconsistencies. Contract tests catch the drift between what a consumer expects and what the provider has become.

Collapse

What is consumer-driven contract testing?

Expand

Why do integration tests pass when services are actually incompatible?

Expand

What does the provider team gain from contract testing?

Expand

Where should a team start with contract testing?

Expand

Want to know where your service boundaries are breaking?

Connect your repo and get a free engineering health diagnosis. We identify untested service contracts and the integration gaps your current suite cannot catch.

Get Your Free Diagnosis

Share this article

Help others discover this content

TwitterLinkedIn
Categories:TestingEngineering PracticesMicroservices