Skip to main content

Consumer-Driven Contract Testing

What This Concept Is

Consumer-driven contract testing (CDC) is a testing practice in which each consumer of a service declares, in executable form, exactly what it needs from the provider. The provider runs those declarations as tests against its own service on every build. If a provider change breaks any consumer's expectations, the provider's CI fails -- before the breaking change reaches production.

Key properties:

  • The contract is owned by the consumer (not the producer), because only the consumer knows what it actually uses.
  • The contract is executed by the producer, because only the producer can verify its current implementation against it.
  • The contract covers only what the consumer uses, not the full API surface.

Tools: Pact (by consumer test framework + broker), Spring Cloud Contract, Postman's mock servers. Pact is the reference.

Why It Matters Here

Contracts documented in OpenAPI or event schemas (concept 08) describe the intended shape. CDC tests verify that the shape is actually delivered, on every commit, for every consumer. The difference is the one between "we have a README" and "CI fails on breaking change."

In a microservices architecture, you cannot realistically run end-to-end tests of every consumer for every producer build. CDC replaces that with a faster, localized verification.

Concrete Example

Inventory (consumer) depends on the Orders endpoint from concept 08. With Pact, the consumer writes a test like:

// Inventory service, consumer-side test
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
const { like, uuid, integer, eachLike } = MatchersV3;

const provider = new PactV3({ consumer: 'inventory-svc', provider: 'orders-svc' });

test('fetch confirmed order for reservation', async () => {
provider
.given('order 123 is confirmed')
.uponReceiving('a request for order 123')
.withRequest({ method: 'GET', path: '/orders/123' })
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: like({
id: uuid('00000000-0000-0000-0000-000000000123'),
status: 'confirmed',
items: eachLike({ sku: 'SKU-1', quantity: integer(2) }, { min: 1 }),
}),
});

await provider.executeTest(async (mock) => {
const res = await fetchOrder(mock.url, '123');
expect(res.status).toBe('confirmed');
expect(res.items.length).toBeGreaterThanOrEqual(1);
});
});

This test:

  1. Spins up a local mock of the Orders service.
  2. Declares "when I GET /orders/123 in the state 'order 123 is confirmed', I expect this shape back."
  3. Produces a pact file (JSON) describing the interaction.

The pact file is published to a broker (Pactflow or self-hosted). The Orders service, on every CI build, fetches all pacts from all its consumers and runs them against its actual implementation:

Orders CI: verify-pacts
consumer: inventory-svc
✓ GET /orders/123 given "order 123 is confirmed" -> 200 ok
consumer: fulfillment-svc
✓ GET /orders/123 given "order 123 is confirmed" -> 200 ok
consumer: analytics-svc
✗ GET /orders/123/events given "order 123 is confirmed" -> 404 <-- regression

The third consumer's expectation fails, so Orders' build fails. The breaking change is caught at producer build time, not in production.

Common Confusion / Misconception

"We have integration tests, so we do not need CDC." Integration tests test against a live provider at one moment in time. They do not protect against the provider changing tomorrow. CDC binds the provider to the consumer's expectations on every subsequent build.

"CDC tests the whole API." No -- it tests only what each consumer actually uses. Endpoints and fields that no consumer expects can still be evolved freely. That is a feature.

"The provider writes the contract." That is producer-driven, not consumer-driven. It tests what the provider meant to deliver, not what any consumer actually depends on.

How To Use It

  1. For each consumer, list the provider interactions it actually uses (endpoints, event types, fields read).
  2. Write a Pact-style consumer test for each interaction.
  3. Publish the pact to a broker.
  4. Configure the provider's CI to pull all pacts and run them as part of its test suite.
  5. Make "pacts passing" a merge gate for the provider repo.
  6. Teach teams: if you need a new field, add it to your consumer test first; the provider build will fail, and that failure is the prompt to update the provider.

Check Yourself

  1. Why is the consumer, not the provider, the right owner of the contract?
  2. What would go wrong if the provider team wrote "consumer" tests on behalf of each consumer?
  3. How does CDC change the feedback loop for a breaking change compared to end-to-end tests?

Mini Drill or Application

Take the sync contract from the concept-08 drill. In 15 minutes:

  • Sketch one consumer-side Pact-style test covering one endpoint and one provider state.
  • List three expectations that, if broken on the provider, should fail this test.
  • Decide: which party's CI fails if any of those three regress, and why.

Evolution and Versioning in CDC

  • Providers can still roll out changes safely: check the pact matrix across consumer versions, ensure all supported consumer versions pass before rolling out.
  • For events, CDC analogs exist (Pact supports async messages; schema registries enforce compatibility at publish time). Same principle: consumer declares what it needs, producer verifies.

How This Sits In The Module

CDC makes the contracts from concept 08 executable and enforceable. Together with database-per-service (concept 07), it gives you enough decoupling to support independent deployment (concept 14).

Async Contract Testing (Message Pacts)

CDC extends naturally from sync APIs to async events. The consumer-side pact declares "when I receive a message of shape X, my handler must process it correctly"; the producer verifies it emits messages of shape X.

// Consumer-side message pact (Pact v3)
import { MessageConsumerPact, synchronousBodyHandler, MatchersV3 } from '@pact-foundation/pact';
const { like, uuid, integer, eachLike } = MatchersV3;

const messagePact = new MessageConsumerPact({
consumer: 'inventory-svc',
provider: 'orders-svc',
});

test('reserves stock on OrderConfirmed', async () => {
await messagePact
.given('an order with two SKUs is confirmed')
.expectsToReceive('an OrderConfirmed event')
.withContent({
event_version: integer(1),
order_id: uuid(),
items: eachLike({ sku: like('SKU-1'), quantity: integer(2) }, { min: 1 }),
})
.verify(synchronousBodyHandler(handleOrderConfirmed));
});

Producer side: the provider CI pulls the pact, publishes a test event matching the shape, and confirms the consumer's handler pattern would accept it. Schema registries (Confluent, AWS Glue) perform a similar role at publish time -- but they do not test consumer behavior; CDC is complementary.

CDC in Context: The Test Pyramid

Mike Cohn's test pyramid and Martin Fowler's TestPyramid place unit tests at the base, integration next, and end-to-end at the tip. In microservices, CDC replaces most of what would otherwise be end-to-end tests:

  • Unit tests. Internal service correctness.
  • Component tests. A single service tested with all downstreams mocked; uses the same contract fixtures CDC generates.
  • CDC tests. Every consumer's expectations verified against every producer build.
  • End-to-end tests. A handful of smoke tests through the full system. Expensive, flaky; keep small.

A mature microservices stack has dozens of CDC tests for every one end-to-end test. The inversion is the point: end-to-end tests cannot scale with service count; CDC tests scale linearly and in parallel.

Read This Only If Stuck

Local chunks

External canonical references

Depth Path

  • Chris Richardson, Microservices Patterns, section on testing strategies -- puts CDC next to contract tests, component tests, and end-to-end tests so you know where each fits.
  • Martin Fowler, TestPyramid -- the broader test-strategy frame CDC slots into.
  • Lewis Prescott, Contract Testing in Action (Manning) -- a practitioner-level book for Pact in depth.