Contract testing with Pact
The answer to "integration tests across six services are too slow and too flaky". Each side verifies its half of the agreement independently. Hard to introduce socially, enormously valuable once it lands.
Contract testing checks that two services still agree about the shape of their conversation, without running them together.
The consumer records what it needs from a response. That recording is the contract, or in Pact's language a pact file. The provider then replays those expectations against itself and confirms it still satisfies them.
Nothing runs end to end. The consumer's tests run against a local stub built from the contract. The provider's tests run against the contract file. A shared broker stores the contracts and tells each side whether it is safe to deploy.
That last part is the real product: an answer to "can I deploy this without breaking anyone".
The terms you will hear
- Consumer. The service or app that calls the other one.
- Provider. The service being called.
- Pact file. The recorded expectations, generated by the consumer's tests.
- Broker. A shared service storing contracts and verification results.
- Can-I-deploy. A broker query answering whether a version is safe to release.
- Provider states. Preconditions the provider sets up to satisfy an expectation, such as "a gift card with 25.00 exists".
What problem it actually solves
Without it, teams verify integrations by deploying everything to a shared environment and running end-to-end tests. That approach has three familiar costs.
- The environment is shared, so one team's broken deploy blocks everyone.
- Failures are slow to attribute, because a red test names a journey rather than a service.
- Coverage is thin, because end-to-end tests are expensive so there are few of them.
Contract tests move that verification to each side's own pipeline, in milliseconds, with a clear owner for every failure.
Why it matters, and when it does not
For example, a mobile app reads orderTotal from a checkout response. The API team renames it in a tidy-up. Without contracts, that reaches a shared environment, the app's end-to-end test fails two days later, and the argument about who broke it takes an afternoon. With contracts, the provider's own pipeline fails on the rename, before merge, naming the consumer.
When it is not worth it: a single deployable application, or two services owned by the same three people who deploy together. There the overhead of contracts and a broker exceeds the coordination problem they solve. Schema validation in your API tests gets you most of the value for far less machinery.
How to introduce it
- Start with one pair. Your most painful consumer and provider, not all of them.
- Write the consumer test first. It states what the consumer needs and produces the contract as a by-product.
- Publish to a broker with the version and the branch. Without a broker you have files, not a workflow.
- Verify on the provider side in its own pipeline. A provider change that breaks a contract should fail before merge.
- Add can-i-deploy to both pipelines. This is where the value lands, because it converts contracts into a release gate.
- Keep contracts about shape and agreed values. Business rules belong in the provider's own tests.
- Delete contracts for retired consumers, or the provider is held to promises nobody needs, which is the same rot as an unculled pack.
A worked pair
For example, here is one consumer expectation and what it caught.
CONSUMER mobile app PROVIDER orders API
WHAT THE CONSUMER DECLARED IT NEEDS
given "a gift card with 25.00 exists for the customer"
upon POST /orders/redeem { orderId, code }
expect status 200
body contains:
remainingToPay string, like "10.99"
giftCardBalance string, like "0.00"
paymentLines array, at least 1, each { method, amount }
note the app ignores the other 9 fields in the response.
the contract says nothing about them, deliberately.
GENERATED pact file published to the broker, tagged main, version 2.14.0
PROVIDER SIDE runs in the API's own pipeline
sets up the provider state, replays the expectation, confirms it holds.
WHAT IT CAUGHT, TWICE IN SIX MONTHS
1 a tidy-up renamed remainingToPay to amountDue.
the provider pipeline failed before merge, naming the mobile app.
fix: keep both fields for one release, then coordinate removal.
cost: 10 minutes and a conversation.
without contracts: a broken app in production, found by customers.
2 paymentLines became nullable when an order was fully covered.
the app crashed on null. the contract said "array, at least 1",
so the provider's verification failed.
fix: return an empty array rather than null.
WHAT IT DID NOT CATCH
the 1p rounding defect. the shape was correct and the number was
wrong. that is what the API value tests are for, and contracts were
never going to help.
OVERHEAD about two days to set up, then near zero per change.The last block matters as much as the successes. Contracts prove the conversation still works. They say nothing about whether the arithmetic inside it is right.
How to show you know it
- One working pair with a broker. Both pipelines, plus can-i-deploy. That is the whole practice.
- A rename caught before merge, naming the affected consumer. It is the canonical win.
- A minimal contract. Showing that you declared only the three fields you use demonstrates you understand the constraint direction.
- An honest boundary. "Contracts caught the shape drift; the rounding bug needed value tests." That sentence is what stops a team over-trusting them.
Questions
Is this the same as schema validation?
Related and not the same. Schema validation checks a response against a shape you declared. Contract testing makes that shape a shared, versioned agreement between two teams, with a deploy gate attached.
Do we need Pact specifically?
No, though it is the most common tooling and has a mature broker. The pattern matters more than the library, and any approach that gives you a versioned consumer expectation plus provider verification will do.
Who owns fixing a broken contract?
Whichever side changed. That clarity is much of the value, because a red end-to-end test names a journey while a failed contract names a service and a version.
Does it replace end-to-end tests?
It replaces most cross-service integration checks and not the handful of journeys that prove a user can really do the thing. Keep those few, as in pyramid or trophy.