A spec and a contract test are not the same gate

You have an OpenAPI file, both sides are generated from it, and your services still broke each other. The spec was never lying. It just permits more than anybody relies on.

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.

What the spec permits against what a client depends on A large dashed region shows every response the specification allows. A smaller solid region inside it shows the responses one client actually relies on. A change that moves a behaviour out of the smaller region while staying inside the larger one still matches the specification and breaks that client. what the spec permits what this client depends on 409 400 still legal the spec still passes; the screen that had to say "that desk just went" does not
A specification is deliberately permissive. It says what may happen, which is a larger set than what anybody is relying on. Illustrative — the shape of the argument, not measured data.

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.


Working through this in your own team?

I help engineering teams adopt AI coding assistants without giving up engineering rigour, and design the architecture underneath. If that's on your plate, let's talk.