Your two services agree about a schema. The file is checked in, both sides generate from it, and neither can name a field the other does not have. Then a release goes out and the front end shows the wrong thing anyway, and the spec is still, exactly, correct.
That combination is confusing enough that teams usually conclude somebody skipped a step. Nobody did. A specification describes what an API may do, and that is a larger set than what any particular caller is relying on.
The change that proves it
Here is the one I ran deliberately, to find out whether the gap was real.
Booking a desk that is already taken answers 409. The spec documents 400 and
409 on that endpoint, because both are genuinely possible: a malformed body is
one, a conflict is the other. So I changed the conflict path to answer 400.
| what checked it | what it said |
|---|---|
| the OpenAPI file | fine, 400 is documented |
| the generated server interface | unchanged |
| the generated client types | unchanged |
| the unit tests | green |
| the browser | shows a generic error and loses the date the member had chosen |
Nothing in either build noticed. Both sides were individually correct, both were green, and they disagreed.
What the pact adds
A consumer contract is smaller and stricter than a spec, because it is written from the other end. It does not describe the API, it records what one caller would notice if it changed.
1) Verifying a pact between deskspace-frontend and deskspace-backend
- a booking for a desk that is taken: has status code 409
1.1) status: expected status of 409 but was 400
That is the only thing in either repository that said anything.
The part that stops it being annoying
A contract test that is strict about everything is a test somebody deletes. So it has to be strict about the coupling and loose about everything else, and knowing which is which is a design decision, not a mechanical one.
In the same service, changing the problem title from “Desk already booked” to “Conflict” does not fail verification, and it should not. The screen renders whatever title the API sends, which is written down as a rule: failure wording belongs to the API, not to the client, so it is worded once instead of three times in three clients. The title is therefore not part of the coupling.
The status is. So the pact pins the status and the shape, and matches the title by type.
Why not just tighten the spec
The obvious alternative is to delete 400 from that endpoint so 409 is the
only failure. It works for this one case and it makes the document worse.
400 is genuinely possible there. Removing it to make the spec behave like a
test means the spec is now false, and the next person reading it to find out what
the API does gets a wrong answer. A permissive document plus a real gate beats a
document that has been bent into a gate, for the same reason
a script beats a paragraph:
each thing should be doing the job it is good at.
What this costs
Provider verification needs the states each interaction assumes, set up through the real code paths rather than by writing rows directly. That is a real cost and it is where most of the work is.
One thing that is not optional: pin the clock. A pact names a specific date, and with a real clock that date drifts into the past until the service starts rejecting it, and the build goes red having been touched by nobody. A red build that nobody caused is the fastest way to teach a team to ignore a suite.
If your services agree on a schema and disagree in production, that’s the work I do.