Consumer-driven contract testing is a well-established practice. The Pact framework has been around since 2013. Most teams doing serious microservices work have encountered it. Yet the most common deployment of contract testing still puts it in the CI pipeline — after the PR merges into the main branch, often as a pre-deploy gate.
That placement is better than nothing, but it's the wrong moment. By the time a contract break is detected in post-merge CI, the change has already been reviewed and approved. The author has mentally moved on. The reviewer has already forgotten the context. If the break is serious enough to block deploy, you're now coordinating a hotfix under time pressure.
Moving contract validation to PR time — before merge, while the author and reviewer are still in context — changes the cost curve entirely.
What consumer-driven contract testing actually means
Before getting into the when, it's worth being precise about the what. Consumer-driven contract testing is a pattern where the consumer of an API defines a contract — a specification of what responses it expects — and the provider verifies that it satisfies those contracts.
The most common implementation uses a contract broker: the consumer publishes a "pact" (a JSON document describing expected request/response shapes) to a shared broker, and the provider runs verification against those pacts as part of its own test suite.
The key property of consumer-driven contracts is that they encode actual usage, not just the provider's view of its own API. If a consumer only uses three fields from a 20-field response body, the pact captures that — and the provider knows it can safely change the other 17 fields without breaking that consumer. This makes contracts much more precise than just diffing OpenAPI specs, which tell you what changed but not whether any consumer actually cared.
The standard pipeline and where it breaks down
In the typical Pact-based workflow, the sequence looks roughly like this:
- Consumer team adds a new expectation → publishes pact to broker.
- Provider team modifies an endpoint → opens a PR.
- PR merges → CI runs provider verification against published pacts.
- Contract break detected → CI fails, deploy blocked.
- Provider team re-opens context, investigates, fixes, re-merges.
Step 4 is the failure in the wrong place. The problem with post-merge detection is that "deploy blocked" is a different kind of alert than "PR comment before merge." Post-merge means you're coordinating between the provider team's now-closed PR, the consumer team's published pact, and your deployment pipeline — often across organizational boundaries, with different on-call schedules, and under time pressure if you're in a release window.
Pre-merge means: the author gets a PR comment saying "this change breaks the pact from service X. Here are the affected fields." They fix it in the same PR. The reviewer sees the fix alongside the original change. Everything stays in context.
The three classes of contract break
Not all contract breaks are equal in how detectable they are and how quickly they need to be caught. Understanding the breakdown helps you prioritize what to validate at PR time.
Field removal and renaming. A field that consumers reference disappears from the response, or changes its key. This is immediately detectable from a diff of the response schema and is the easiest class to catch with static analysis. A consumer that dereferences response.account_status will fail if account_status is removed or renamed to status.
Type and format changes. A field that was a string becomes an integer, or a date format changes from ISO 8601 to Unix epoch. Type changes are also structurally detectable if you're parsing the schema. Format changes within a type are harder — you need semantic analysis of how the value is used, not just that the field exists.
Behavioral contract changes. The field exists, the type is the same, but its semantics have changed. An amount field that used to be in cents now returns dollars. A status field that used to be an enum with four values now has six — and consumers that had exhaustive switch statements on four values now have unhandled cases. These are the hardest to detect statically and are the class where consumer-driven contracts (which capture actual usage patterns) provide the most value over spec diffing.
What "at PR time" requires technically
Running contract verification at PR time — rather than only in CI after merge — requires a few things that aren't always in place:
A pact broker (or equivalent) that's queryable by the analysis pipeline. Most teams using Pact already have this. The question is whether the PR-time analysis can read from it. In practice, this means the broker needs to be reachable from wherever PR analysis runs, with appropriate read access.
The ability to extract the effective API schema from the PR diff. For OpenAPI-defined endpoints, this usually means parsing the spec file change in the diff and constructing a before/after diff of the schema. For endpoints without a spec, it requires inferring the schema from the code — which is possible for many typed languages (TypeScript, Go, Java with annotation-based routing) but requires language-specific AST parsing.
Consumer mapping: which services consume which endpoints. You can't flag a contract break if you don't know who the consumers are. This information lives in the pact broker, or can be inferred from codebase analysis of service-to-service call patterns. Either approach works; the key is that it's maintained and queryable.
When these three pieces are in place, PR-time contract checking becomes: parse the diff, extract changed response shapes, look up consumers for affected endpoints, compare expected shapes against new shapes, post inline comments on the relevant diff lines if breaks are found.
A scenario: a payments API breaking change
Consider a plausible scenario: a mid-size payments platform with eight backend services. The accounts-service is refactoring how user account metadata is structured. A PR modifies GET /v2/accounts/{id} to remove the legacy_plan_code field (deprecated 14 months ago, migration guide published, supposedly all consumers updated).
The problem: the billing-service still references legacy_plan_code in a code path that handles annual renewal processing — a path that runs only on subscription anniversary dates, so it doesn't appear in normal integration test runs. The engineer who wrote the migration guide didn't check that specific path. The reviewer of the accounts-service PR didn't know about it.
Without PR-time contract checking: the PR merges, post-merge CI passes (billing-service's CI doesn't exercise that path in its standard suite), the change ships. The break surfaces when annual renewals start failing, which could be weeks later if the change happens to ship just after the last anniversary batch.
With PR-time contract checking: the analysis reads the pact published by billing-service, finds that legacy_plan_code is referenced in the pact's expected response for this endpoint, and posts an inline comment on the line removing the field: "billing-service has an active pact that references this field. The field is used in 1 consumer pact. Consider verifying migration is complete before removing."
The fix is one conversation, before merge, with the author in context. Instead of an incident investigation weeks later.
The objection: this will add noise
The reasonable concern with any PR-time validation is false-positive noise. Engineers stop reading automated PR comments if they're wrong half the time. This is a legitimate concern, and it argues for precision over recall in contract break detection.
In practice, the highest-precision signal is field removal against active pacts: if a consumer has an active pact that references a field, and the PR removes that field, that's a near-certain break. The false positive rate on this class is very low. Lower-precision signals — like inferring behavioral contract changes from usage pattern analysis — should be flagged at a lower severity and with explicit uncertainty ("this may affect consumers that depend on this value being non-negative").
The goal is to make contract checking actionable, not exhaustive. A PR comment that says "this breaks 2 consumers, here's which fields" gets acted on. A comment that says "15 possible semantic concerns detected" gets dismissed.
Where CI-based contract testing still fits
Pre-merge contract checking doesn't make post-merge CI contract verification redundant. They serve different purposes.
Pre-merge analysis is fast, targeted at the changed surface, and designed for developer feedback: it tells the author and reviewer about breaks while they can still fix them in context.
Post-merge CI verification is comprehensive — it runs all pacts, including pacts from services you didn't know were consuming your API (if you have an open broker). It's the safety net that catches anything the pre-merge analysis missed or that was introduced by a merge conflict resolution.
The right architecture runs both. Pre-merge provides the feedback loop that prevents most breaks from reaching main. Post-merge provides the guarantee that nothing slipped through. The two layers are complementary, not redundant.
The teams that have the fewest contract-break incidents are not the ones who run the most aggressive CI pipelines — they're the ones who surface contract information to engineers at the moment when engineers are making the decisions that create or prevent breaks. That moment is the PR, not the deploy queue.