A consumer's contract file is regenerated from its own test suite on every build and published to the broker. A developer on the consumer team deletes an obsolete test that happened to be the only one exercising the refund_reason field. Nobody notices at the time. What happens over the following two months?
Show the full answer Hide the answer
Step by step
Day 0. The consumer's build is green and publishes a contract with one fewer expectation. Nothing in the pipeline is designed to react to an expectation disappearing, because a verification suite can only fail on expectations that exist. Removing one strictly reduces the number of ways the provider's build can go red.
Day 0 to 40. The provider's verification stays green. The deployment compatibility check stays green. Every signal the practice produces says the integration is safe, and it is measurably safer than it was, in the sense that fewer things are being checked.
Day ~45. A provider engineer removing dead fields greps the broker for consumers of refund_reason, finds none, deletes it. This is the correct procedure and it produces the wrong answer.
Day ~46. The consumer's production code still reads the field. Depending on the deserialiser it gets a null, a default or a swallowed exception. Nothing errors loudly, because a missing JSON field is not an error in most client libraries.
Day ~60. Someone notices that refund reasons have been blank in the finance export since the middle of last month.
The mechanism people miss
A generated contract is a derivative of the consumer's tests, not of the consumer's code. Consumer-driven contract testing silently assumes those two things are the same set, and they never are. The gap is the share of response fields the consumer's code reads that its tests never touch - typically a large minority, and entirely invisible.
This is the practice's structural weakness, and it is the mirror image of its strength. The reason it can tell a provider "you may safely remove this field" is precisely that it claims to know everything consumers use. The claim is only as good as consumer test coverage of the response.
What stops it
- Treat a shrinking contract as a reviewable event. Diff the published contract against the previous version on every publish, and require the same review a deliberate API change gets when an expectation is removed. This is one comparison in the publish step and it catches the case at day 0.
- Measure contract coverage directly: the proportion of response fields the consumer's code deserialises that the contract also asserts on. Derive it from the consumer's type definitions or deserialisation code, not from test coverage, because test coverage is the thing that lied.
- Ground removals in production, not in contracts. The provider logs field-level access per consumer and refuses to remove any field with non-zero reads in the last 30 days. This is the only check that observes what is actually used rather than what someone wrote a test for, and it is the one to build first if you build only one.
- Fail closed on the consumer side. A deserialiser that rejects a missing required field converts a silent blank column into a loud startup or request failure, moving detection from day 60 to day 46.
When not to build this machinery
Two teams, one API, a handful of fields, deployed together: a shared schema file and a conversation cost less than a broker, a publish step, a coverage metric and a review gate. Consumer-driven contracts earn their cost when consumers outnumber the people a provider can talk to in a morning, and the coverage gap above is the reason they do not remove the need to talk.