Tax / Fiscal · Contract

Consumer-Driven Contracts — A Field Guide

Consumer-Driven Contracts — A Field Guide is the work that makes the systems talk. The API is the contract between the producer and the consumer; the contract is what determines.

John Kihiu12 min read

Consumer-driven contract testing flips the usual integration-test problem around. Instead of the API provider guessing what its consumers need and writing tests against its own assumptions, each consumer publishes a contract — a concrete set of expectations about requests and responses — and the provider verifies its implementation against every contract it has agreed to serve. The result is a test suite that catches breaking changes before deployment, without needing a shared staging environment where every service is running at once.

The problem with end-to-end integration tests

Most teams start with end-to-end tests: spin up every service, run a scenario through the whole stack, assert on the final state. It works until the number of services grows past four or five. Then the suite becomes flaky for reasons that have nothing to do with the code under test — a downstream service is slow to boot, a shared database has stale fixtures, a network call times out under CI load. Worse, a failure tells you *something* broke somewhere in the chain, not *what* broke or *whose* fault it was. Contract tests solve a narrower problem on purpose: they only verify the boundary between two services, and they do it without either service needing the other running.

How Pact-style contract testing works

The consumer side runs first. The consumer team writes tests against a mock of the provider, using a library like Pact, and those tests generate a contract file — a JSON document describing each interaction: the request the consumer will make and the response it expects back. That contract is published to a broker (Pact Broker, or a shared artifact store). On the provider side, a verification step replays every published contract against the real provider implementation and checks the actual response against what was promised. If the provider changes a field name or drops a field a consumer depends on, verification fails before the change ships.

JAVASCRIPT · CONSUMER CONTRACT TEST
const { PactV3, MatchersV3 } = require('@pact-foundation/pact');
const { like, integer } = MatchersV3;

const provider = new PactV3({
  consumer: 'InvoiceService',
  provider: 'CustomerService',
});

describe('GET /customers/:id', () => {
  it('returns the customer record', () => {
    provider
      .given('customer 42 exists')
      .uponReceiving('a request for customer 42')
      .withRequest({ method: 'GET', path: '/customers/42' })
      .willRespondWith({
        status: 200,
        body: { id: integer(42), name: like('Acme Ltd'), creditLimit: like(5000) },
      });

    return provider.executeTest(async (mockServer) => {
      const res = await fetch(`${mockServer.url}/customers/42`);
      expect(res.status).toBe(200);
    });
  });
});

Matchers, not exact values

The example above uses like() and integer() rather than hardcoded values. That distinction is the whole point of a contract test: the consumer is asserting on *shape*, not on specific data. If the consumer asserted name === 'Acme Ltd' literally, the contract would break the moment the provider's test fixtures changed, even though the actual API behaviour is unaffected. Type and structure matchers let the contract express "a string will be here" rather than "this exact string will be here," which is what the consumer actually depends on.

Contract tests are not a substitute for a handful of real integration tests

A contract test proves the provider still returns what it promised. It does not prove the whole system behaves correctly end-to-end — that the invoice actually gets created, that the event actually gets processed. Keep a small number of true end-to-end smoke tests for the critical paths, and let contract tests carry the weight of everything else.

Wiring verification into CI

The mechanical part that makes this pay off is the CI wiring. The provider's pipeline pulls the latest contracts for every known consumer from the broker, runs verification against the current build, and publishes the verification result back to the broker. The consumer's pipeline, before deploying, asks the broker "can I deploy safely?" — the can-i-deploy check — which looks at whether the currently deployed provider version has verified this consumer's contract. This is what actually prevents the breaking change from reaching production: not the test itself, but the deployment gate built on top of it.

Start with your highest-traffic integration

Contract testing has setup cost — a broker, CI wiring on both sides, buy-in from the provider team to run verification on every change. Don't try to cover every service pair on day one. Pick the integration that breaks most often or causes the most painful incidents, prove the workflow there, then expand.

Who owns the contract

The name "consumer-driven" is doing real work: the consumer defines what it needs, and the provider's job is to keep serving it — not the other way around. This inverts a common assumption that the provider team designs the API and consumers adapt. In practice it means provider teams need a way to see, at a glance, which fields and endpoints are actually depended on by which consumers, so a "harmless" refactor doesn't silently break someone downstream. A contract broker's dashboard is usually where that visibility lives, and it becomes a genuinely useful map of your service dependency graph — often more accurate than any architecture diagram, because it's generated from what teams actually test.

ApproachCatchesCost
End-to-end integration testsFull-stack behavioural regressionsHigh — flaky, slow, needs shared environment
Consumer-driven contract testsBreaking changes at a service boundaryMedium — broker + CI wiring, no shared environment
Schema-only validation (OpenAPI diff)Structural drift in the specLow — but misses behavioural mismatches
Manual coordination between teamsWhatever someone remembers to checkLow upfront, high in incidents

Wrapping up

Consumer-driven contracts work because they test the thing that actually breaks production: the boundary between two services, verified from the consumer's actual expectations rather than the provider's assumptions. Start with one high-value integration, get the broker and the can-i-deploy gate wired into CI on both sides, and expand from there — the payoff compounds as more service pairs adopt it, because each new contract is one more thing you no longer have to coordinate manually before a release.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.