The usual way teams catch integration bugs between services is a shared staging environment where everything is deployed together and a suite of end-to-end tests runs against the lot. It works, but it's slow, flaky, and tells you a service is broken without telling you which contract it violated. Pact takes a different approach: consumer-driven contract testing, where the consumer of an API writes down exactly what it expects, and the provider verifies against that expectation independently — no shared environment required.
How Pact actually works
The consumer side runs its normal unit tests against a mock provider that Pact spins up, and while doing so, the Pact library records every interaction — the request the consumer sent and the response it expected — into a contract file (the "pact"). That file is the artifact: a JSON document listing concrete request/response pairs the consumer actually relies on, not a generic schema. The provider side then replays every interaction in that file against the real provider service and asserts the actual response matches what the consumer expected. If a provider team ships a change that would break a consumer's expectation, the provider verification test fails in the provider's own CI pipeline — before the change ever reaches a shared environment.
describe('Order service client', () => {
it('returns an order by id', async () => {
await provider.addInteraction({
state: 'an order with id 123 exists',
uponReceiving: 'a request for order 123',
withRequest: { method: 'GET', path: '/orders/123' },
willRespondWith: {
status: 200,
body: {
id: '123',
status: 'shipped',
total: like(42.50) // matcher: type-match, not exact value
}
}
});
const order = await orderClient.getOrder('123');
expect(order.status).toBe('shipped');
});
});
// Running this test generates orderclient-orderservice.json —
// the actual contract, checked in or published to the Broker.
The provider verification side
On the provider's CI, a Pact verification step pulls the contract file and replays each recorded request against a real instance of the provider (usually with test data set up per the "state" the consumer specified, like "an order with id 123 exists"). It's a normal integration test from the provider's point of view, except the test cases come from consumers instead of being written by the provider team — which is the point: the provider finds out exactly which real consumer behavior it would break, instead of guessing from a spec document nobody kept in sync.
Traditional API contracts are usually written by the provider team and consumers are expected to conform. Pact inverts that: the contract is defined by what consumers actually call and depend on, which tends to be a much smaller, more honest surface than the full API surface the provider thinks it needs to support. Providers can safely change or remove parts of an API that no registered consumer contract touches.
The Pact Broker
The Broker is the piece that makes this scale past two services talking directly. Consumers publish their contracts to the Broker after each CI run; providers query the Broker for all the contracts that name them as the provider, verify against every one, and publish results back. The Broker also computes a compatibility matrix — "can I safely deploy version X of this consumer against version Y of that provider" — which is what CI/CD pipelines query before deploying, a check commonly called "can-i-deploy." Without the Broker, you'd need to manually track and pass contract files between repos, which doesn't survive more than a couple of services.
pact-broker can-i-deploy \
--pacticipant OrderServiceClient \
--version $GIT_SHA \
--to-environment production \
--broker-base-url https://pact-broker.internal
# Exits non-zero if any provider hasn't verified this consumer
# version yet, or if verification failed -- blocking the deploy
# before it reaches production, not after.
Where it fits vs. schema-based contract testing
Schema-based approaches — validating requests and responses against an OpenAPI or JSON Schema document — catch structural drift (a field renamed, a type changed) but say nothing about whether the specific values and interactions a consumer relies on still behave correctly; a schema can stay technically valid while the actual business logic behind it breaks a consumer's real usage. Pact catches the latter because it tests real interactions, not just shapes, but it requires consumers to actually write out their expectations, which is more upfront work than pointing a validator at an existing spec. In practice teams often use both: schema validation as a cheap first-pass check, Pact for the handful of cross-team integrations where a subtle behavioral break would actually hurt.
Contract tests confirm the shape and behavior of an interaction between two services in isolation — they don't validate that the whole system behaves correctly end to end, and they don't replace unit tests of business logic. Teams that skip a smaller, later-stage E2E smoke suite entirely on the assumption that "Pact covers integration" still get burned by whole-system issues Pact was never meant to catch — it verifies pairwise contracts, not system-wide emergent behavior.
Wrapping up
Pact moves integration testing from "deploy everything to staging and see what breaks" to "each provider verifies, in its own CI, against the actual expectations of every consumer that depends on it." The Broker is what makes that tractable across more than a couple of services, since it tracks which consumer and provider versions are compatible and gates deploys on it. It's not a replacement for unit tests or a smaller end-to-end smoke suite — it's specifically aimed at the class of bug that a shared staging environment used to be the only way to catch.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.