Engineering Practice
Consumer-driven contracts: the consumer states the truth
Priya owns the checkout-web frontend; Sam owns the pricing API. Sam keeps a beautiful OpenAPI doc, but it’s been wrong twice this quarter — once it described a field that was never shipped, once it missed a tax_cents field three teams already depended on. The doc isn’t executed by anything, so nothing forces it to match the code. Priya’s team finally stops reading Sam’s doc and instead writes a tiny test: “when I GET /prices/42, I read amount_cents (a number) and currency (a string).” That test, run once, emits a JSON file describing exactly that interaction. Now there is an artifact that says what checkout-web actually needs — and it can be replayed against Sam’s real service to prove it still holds.
Who authors the truth, and why it’s the consumer
In the previous lesson the missing layer had to check the boundary without booting both services. The first design decision is: who declares what the boundary should be? Two answers exist. Provider-driven (spec-first): the provider publishes an OpenAPI document and consumers are expected to conform. Consumer-driven: each consumer declares, in executable form, exactly the requests it makes and the parts of the response it reads, and the provider is obligated to honour the union of those.
Consumer-driven wins for a specific reason: it captures actual usage, not declared surface area. Sam’s pricing API might return forty fields; checkout-web reads four. A consumer-driven contract records those four. That makes the contract a precise statement of “what would actually break someone” — Sam can freely change the other thirty-six fields, and only a change to one of the four trips the gate. A provider-driven spec, by contrast, flags every change equally, including ones no consumer cares about, which generates noise and erodes trust. The consumer is the only party that knows what it truly depends on; consumer-driven contracts make that knowledge executable.
The pact file is a recording, not a wishlist
The artifact at the center of this is the pact file — in Pact, the de-facto open standard, a JSON document. The crucial property is how it’s produced: it is not written by hand, it is generated as a side effect of a passing consumer test. The mechanism:
- The consumer’s test runs against a Pact mock server — a local HTTP server Pact stands up. The test configures it: “expect a
GET /prices/42; respond200with this body.” Then the consumer’s real client code calls that mock. - If the consumer code actually makes the request it claimed and parses the response it claimed, the test passes — and Pact writes the exercised interaction into the pact file. If the consumer’s code drifts (it stops sending
Accept: application/json, or never reads the field), the mock expectation fails and no pact is generated, so you can’t publish a contract you don’t honour.
This is the inversion that makes contracts trustworthy where docs aren’t. A doc is a promise written separately from the code; nothing executes it, so it rots silently. A pact file is a recording of code that ran and passed. It cannot describe an interaction the consumer doesn’t actually perform, because it’s emitted only when the consumer’s own test exercises it.
| Aspect | Hand-kept OpenAPI / wiki doc | Consumer-driven pact file |
|---|---|---|
| Authored by | Provider, declaring full surface | Consumer, declaring actual usage |
| How it’s produced | Written separately from code | Emitted by a passing consumer test |
| Drift risk | Rots silently; nothing executes it | Can’t describe what code doesn’t do |
| Change signal | Flags every field equally (noisy) | Trips only on consumed fields |
Match on shape, not on exact values
A naive recording would over-specify: if the pact pins amount_cents to exactly 1299, then any response with a different price fails verification — even though the consumer doesn’t care about the value, only that it’s a number. Pact solves this with matchers. Instead of “the body equals this exact JSON,” the consumer declares “the body has a field amount_cents that is an integer, a field currency that is a string matching [A-Z]{3}, and an items array with at least one element of this shape.” Verification then checks the type and structure, not the literal example values.
This matters because it draws the contract’s boundary at the right place: it asserts what the consumer genuinely relies on (a number is present and parseable) and stays silent about what it doesn’t (which number). Over-specifying with exact values reintroduces brittleness — the contract fails on benign data changes. Under-specifying (e.g. only checking the field exists, not its type) lets real breakage through. Good contracts use type matchers for the parts the consumer parses and leave everything else unconstrained.
Why this works
Provider states are the other half of why a pact is replayable. A consumer’s interaction often assumes data exists: “GET /prices/42 returns 200” only holds if price 42 exists. The pact records a providerState string like "a price with id 42 exists". During verification (next lesson) the provider sets up that precondition before replaying the request, so the test is deterministic and doesn’t depend on whatever happens to be in a shared database. The contract carries its own setup contract; that’s what lets it run against the real provider in isolation.
Order how a consumer-driven pact file comes into existence:
- 1 Consumer test configures a Pact mock server: 'expect GET /prices/42, respond 200 with this shape'
- 2 The consumer's real client code calls the mock and parses the response
- 3 The test asserts the parsed fields it actually needs, using type matchers not exact values
- 4 The test passes, so Pact writes the exercised interaction (with provider states) into a pact file
- 5 The pact file is published to the broker, tagged with the consumer's version and branch
Why is a consumer-driven pact file more trustworthy than a hand-maintained OpenAPI document?
A consumer reads only that amount_cents is a number. What should its pact assert about that field?
Your provider returns 40 fields; your consumer reads 4. How should the contract be scoped?
- 01Explain the full mechanism by which a pact file is produced, and why that makes it more trustworthy than an OpenAPI doc.
- 02Why match on type rather than exact values, and what role do provider states play?
The missing boundary layer needs someone to declare what the boundary is, and the consumer is the right author because it alone knows what it actually depends on. A consumer-driven contract records actual usage — the few fields a consumer reads out of however many a provider returns — so the provider can freely evolve everything else and the gate trips only on changes that would truly break a real consumer; a provider-driven spec, by contrast, flags every change equally and can drift from the code. The artifact is the pact file, and its defining property is that it’s generated as a side effect of a passing consumer test against a Pact mock server, not written by hand: it can only describe interactions the consumer’s code performs, so it can’t rot the way a doc does. Good contracts match on type and structure rather than exact values — guarding what the consumer parses while staying silent about which value it gets — and carry provider states that let the provider set up preconditions so the pact replays deterministically in isolation. The pact is then published to a broker, tagged with version and branch, ready for the provider to verify — which is the next lesson.