Consumer-Driven Contract Tests for Service Compatibility
Two services can pass their own test suites and still fail when deployed together.
A provider may rename a field, narrow an accepted value, change a status code, or remove an endpoint. Its internal tests can remain green because those tests describe the provider’s own view of correct behavior. A consumer can still depend on the old interaction.
Consumer-driven contract testing turns selected consumer expectations into executable contracts. The consumer records the interactions it requires. The provider then verifies those contracts against its implementation.
The result is a compatibility check at the boundary between independently changing services.
The compatibility problem
Consider an order service that asks a customer service for a shipping profile.
The consumer expects:
GET /customers/42/shipping-profile
HTTP/1.1 200 OK
Content-Type: application/json
{
"customerId": 42,
"postalCode": "10110",
"country": "ID"
}A provider refactor changes postalCode to zipCode:
{
"customerId": 42,
"zipCode": "10110",
"country": "ID"
}The provider may regard this as a cleaner name. The consumer sees a missing field.
Unit tests on each side are not sufficient to expose this mismatch. An end-to-end environment can expose it, but such environments are often slower, more expensive, and harder to diagnose. A contract test targets the interface directly.
A contract represents an observed dependency
A useful consumer contract describes a concrete interaction:
- the request the consumer sends,
- the provider state required for the example,
- the response properties the consumer depends on,
- and any matching rules that permit irrelevant variation.
The contract should not copy the provider’s entire response schema merely because the provider returns it.
Suppose the provider also returns preferredLanguage, createdAt, and marketingSegment. If the order service ignores those fields, requiring exact values for them creates needless coupling.
A focused contract can state that the response must contain an integer customerId, a string postalCode, and a two-character country. Extra fields can remain acceptable.
This distinction matters. A contract is strongest when it captures actual dependency, not every observable detail.
Consumer side: capture required interactions
A consumer test usually runs against a contract mock rather than the real provider.
In pseudocode:
contract.expect(
state = "customer 42 has a shipping profile",
request = {
method: "GET",
path: "/customers/42/shipping-profile"
},
response = {
status: 200,
body: {
customerId: integer(42),
postalCode: string("10110"),
country: string("ID")
}
}
)
profile = shippingClient.fetchProfile(42)
assert profile.postalCode == "10110"The test serves two purposes.
First, it checks that the client sends the request represented by the contract. Second, it checks that the consumer code can process the corresponding response.
A contract artifact is produced from that interaction and made available to provider verification.
Provider side: replay the contract
The provider verification step prepares the named provider state, sends the contract request to the provider, and compares the response with the contract expectations.
Conceptually:
prepareState("customer 42 has a shipping profile")
response = provider.handle(
GET,
"/customers/42/shipping-profile"
)
verify(response, contractExpectation)If the provider now returns zipCode instead of postalCode, verification fails before the incompatible version reaches production.
The failure points to a specific consumer interaction. That is much narrower than an end-to-end failure involving queues, databases, gateways, credentials, and several deployed services.
Provider states make examples reproducible
Contract examples often need provider data. Hard-coding assumptions about a shared database makes verification fragile.
A provider state gives the example a semantic precondition:
"customer 42 has a shipping profile"The provider test harness maps that phrase to setup logic. It may insert a row, configure an in-memory repository, seed a fixture, or stub a downstream dependency.
The contract should state the required condition rather than prescribe storage details. This keeps the consumer from controlling provider internals.
Good provider states are small and deterministic. Avoid states such as "production-like database exists" because they hide too much setup behind a vague label.
Matching rules control coupling
Exact equality is appropriate for values that carry contractual meaning. It is often too strict for generated or variable data.
For example:
{
"requestId": "7e01f4...",
"customerId": 42,
"postalCode": "10110"
}The consumer may require requestId to be a non-empty string but not care about its exact value. A matcher can express that requirement.
Likewise, a consumer may require an array to contain at least one item with a particular shape without fixing the complete array contents.
Use flexible matching only where the consumer is genuinely flexible. Making every field optional or accepting any type turns verification into ceremony without protection.
Contract tests and schema tests are related but distinct
A schema can state that an endpoint returns an object with fields of certain types. That is valuable, but it does not always show which parts a particular consumer uses.
A consumer-driven contract adds interaction context:
Consumer: order-service
Provider: customer-service
State: customer has a shipping profile
Request: GET /customers/42/shipping-profile
Required response: 200 with customerId, postalCode, countryThis creates a compatibility statement tied to a real consumer.
Schema validation remains useful for broad API conformance. Contract verification adds evidence about known service-to-service dependencies. Many systems benefit from both.
Contract tests do not replace end-to-end tests
Contract verification checks boundary compatibility. It does not prove that the whole production path works.
It may not detect:
- incorrect network routing,
- broken credentials,
- deployment configuration errors,
- incompatible infrastructure policies,
- failures across interactions not represented by contracts,
- or business defects spanning several services.
Keep a smaller set of end-to-end checks for critical journeys. Use contract tests to move many interface failures into faster, more focused verification.
The two test types cover different risks.
Version contracts as deployable evidence
A contract becomes much more useful when it is associated with immutable consumer and provider versions.
A practical pipeline can follow this sequence:
- The consumer build publishes contracts identified by its commit.
- The provider build verifies relevant contracts.
- Verification results are recorded against the provider commit.
- Deployment automation checks whether the intended consumer-provider combination has verified evidence.
- Only compatible combinations proceed.
This changes the release question from “Did the provider tests pass?” to “Has this provider version satisfied the contracts required by the consumer versions that matter?”
That is especially useful when services deploy independently.
Select relevant consumer versions deliberately
Verifying every contract ever published eventually creates noise. Old consumers may no longer run anywhere.
Teams need a policy for selecting contracts that still matter. Common inputs include versions currently deployed, versions on active release branches, and versions expected to deploy soon.
The policy should match the organization’s deployment model. A system with one production environment has different needs from a platform supporting many customer-managed versions.
The important property is explicitness: provider verification should cover consumers that can actually interact with the provider.
Compatibility includes request changes too
Response changes receive much attention, but providers can also break consumers by changing accepted requests.
Examples include:
- making an optional request field mandatory,
- rejecting a value previously accepted,
- changing authentication requirements,
- removing a query parameter,
- or changing content-type handling.
A consumer contract records the request it sends, so provider verification can expose these changes as well.
Compatibility is bidirectional at the interaction boundary: the provider must continue accepting required requests and producing acceptable responses.
Keep contracts at the public boundary
A contract should describe observable behavior, not private implementation.
Avoid assertions about:
- internal class names,
- database table structure,
- private method calls,
- internal event objects that never cross the boundary,
- or the exact sequence of internal operations.
Such assertions turn refactoring into a compatibility event even when external behavior stays stable.
A strong contract lets the provider reorganize its internals freely while protecting the interaction consumers rely on.
Handle asynchronous messages with the same principle
The technique also applies to message-driven systems.
Suppose a billing service consumes this event:
{
"type": "OrderConfirmed",
"orderId": "o-817",
"currency": "IDR",
"total": 250000
}The billing service can publish a contract describing the event shape and semantics it requires. The order service, as message producer, verifies that it can emit a matching event.
The transport differs from HTTP, but the core question remains the same: can the producer version satisfy the consumer’s executable expectation?
For asynchronous systems, contract checks should also cover metadata that carries meaning, such as routing keys, event type identifiers, and required headers.
Avoid turning contracts into duplicated integration suites
A contract suite can become slow and brittle if every business scenario is copied into it.
Use contract tests for boundary behavior:
request shape -> accepted provider interaction -> required response shapeKeep rich business rule combinations in unit, component, or domain-level tests where they can run with less setup.
A contract example should earn its place by protecting an integration assumption.
Treat contract changes as interface changes
When a consumer changes its contract, review the change as an API dependency change.
Ask:
- Is the consumer depending on a new field?
- Has a previously flexible matcher become exact?
- Is a new provider state required?
- Does the request now use a new endpoint or parameter?
- Can the currently deployed provider satisfy the new expectation?
This review catches accidental coupling before it becomes part of the release path.
On the provider side, a failed contract should trigger a compatibility decision rather than an automatic update of the expected artifact. Updating the contract merely to make a provider build green defeats the protection.
A compact adoption path
Start with one integration that causes frequent coordination or release failures.
Choose a single high-value interaction and encode only the consumer behavior that matters. Add provider verification to continuous integration. Make failures visible to both service owners.
After the workflow is reliable, add contracts for other important interactions.
Do not begin by converting every API endpoint. Contract testing delivers value through accurate dependency coverage, not contract count.
A useful mental model
Think of a consumer-driven contract as an executable claim:
This consumer sends this kind of request and requires at least this behavior in return.
Provider verification answers a second claim:
This provider version can satisfy that expectation.
Those claims create a precise compatibility signal between independently changing codebases.
The technique is most effective when contracts stay focused on real consumer dependencies, provider states remain reproducible, matching rules avoid accidental coupling, and verification results are tied to deployable versions.
That combination moves interface breakage closer to the code change that introduced it, while preserving independent service evolution.