Contract Testing with Pact for Microservices
Table of contents
- Key Takeaways
- The Problem With E2E Tests at Scale
- What Pact is
- A concrete example
- What it does well, what it doesn't
- Pact Broker workflow
- What Pact Asks of the Process
- Conclusion
- Frequently asked questions
- Do I need a shared staging environment to run Pact contract tests?
- What kinds of bugs does contract testing not catch?
- What is the Pact Broker for, and what are the alternatives?
- Sources
With 30 or more microservices, end-to-end tests become slow, fragile and impractical. Pact implements consumer-driven contract testing: the consumer defines what it expects, the provider verifies it in its own CI pipeline, with no shared environment needed. The result is integration proof in seconds, not minutes.
Any organisation that has migrated to microservices eventually runs into the same problem: end-to-end tests become impractical. When you have 30 services, spinning them all up in a staging environment to run an E2E test is slow, fragile, and expensive. Contract testing solves the problem by moving it to a different layer; its most popular implementation is Pact[1].
Key Takeaways
-
E2E tests with 30+ services have three pathologies: prohibitive setup, compound fragility, and late feedback.
-
Pact implements consumer-driven contract testing: the consumer defines the contract, the provider verifies it in its own CI.
-
Consumer and provider test in a decoupled way, without a shared environment.
-
Pact shines at catching breaking changes; it doesn’t replace tests for emergent business logic or performance.
-
Pact Broker acts as a hub: it versions contracts and maintains a compatibility matrix for deployment gates.
The Problem With E2E Tests at Scale
E2E tests assume you can deploy a complete environment and exercise a user flow end-to-end. In a monolith or 3–4 services, it works. With 30+ services, three pathologies emerge:
-
Prohibitive setup time. Bringing up the whole stack per test takes minutes or tens of minutes. CI becomes the team’s recurring complaint.
-
Compound fragility. If each service has 1% flakiness, combining them multiplies the false-failure probability. Tests fail with no clear culprit.
-
Late feedback. A change breaks two-service integration. You discover it hours later in staging, not in the PR where it happened.
The industry’s collective response since 2015: contract testing.
What Pact is
Pact[1] implements consumer-driven contract testing. The idea in three steps:
-
The consumer defines the contract. The calling service (e.g., a frontend) describes what response it expects from its provider (e.g., the orders API) for each relevant case.
-
Pact generates a contract file (JSON). It’s published to a central broker (Pact Broker[2]).
-
The provider verifies against the contract. In its own pipeline, the provider service runs contracts published by its consumers and checks it meets each expectation.
Result: consumer and provider test in a decoupled way. Each does so in its own CI, without needing a shared environment. Integration proof exists, but as a contract reviewed by both. This pattern fits directly with the flows Figma describes for design-to-engineering handoff: validate the interface before locking it in.
A concrete example
Suppose a frontend (consumer) calls the orders-api service (provider) to get an order by ID.
On the consumer side, the test writes the expected contract:
// Pact JS (consumer)
await provider.addInteraction({
state: "order 42 exists",
uponReceiving: "a request for order 42",
withRequest: { method: "GET", path: "/orders/42" },
willRespondWith: {
status: 200,
body: {
id: 42,
total: 99.99,
items: Matchers.eachLike({ sku: "ABC", qty: 1 })
}
}
});
The test runs against a Pact mock: passes or fails on the consumer side, generating the contract file.
On the provider side:
# Pact Ruby (provider)
Pact.service_provider "orders-api" do
honours_pact_with "frontend" do
pact_uri "http://pact-broker/pacts/provider/orders-api/consumer/frontend/latest"
end
provider_state "order 42 exists" do
set_up do
Order.create!(id: 42, total: 99.99, items: [...])
end
end
end
The provider’s pipeline downloads the contract, replays it against a running service, and verifies all consumer expectations are met.
What it does well, what it doesn’t
Pact shines at:
-
Catching breaking changes before deploy. If
orders-apirenames a field, the verification test fails before reaching production. -
Cutting CI times. Each service tests in isolation in seconds, not in minutes with giant docker-compose files.
-
Executable documentation. The contract is itself API documentation that doesn’t go stale: it fails if you diverge.
What it doesn’t solve:
-
Emergent business logic. If the bug is in a 5-service composition (e.g., a checkout flow), a two-party contract doesn’t catch it.
-
Performance and concurrency. Contract tests are unit-scale; load testing still needs its own approach.
-
Provider internal schemas. It only validates the interface with consumers; internal structure can change without failing the contract.
Pact Broker workflow
In a multi-team environment, Pact Broker[2] is the hub:
-
The consumer’s CI publishes the contract after its tests.
-
The Broker versions contracts by commit/tag and notifies the provider.
-
The provider’s CI verifies and reports back to the Broker whether it passes.
-
The Broker maintains a compatibility matrix tracking which provider and consumer versions are compatible, useful for deployment gates.
Alternatives: Pactflow[3] (managed broker), or self-host with the pact-broker Docker image[4].
What Pact Asks of the Process
Pact works well when three conditions hold:
-
Ownership culture. Consumers know what they want, providers commit to deliver. Without this, contracts devolve into inter-team fights.
-
Mature CI pipeline. Each service needs its own pipeline publishing and verifying contracts automatically. Without this, value is lost.
-
Agreement on "compatible". Sometimes the provider changes something that breaks only one consumer. Pact forces the conversation, but the team must define the criterion.
Also see how agile methodologies help coordinate teams when coordination emerges from contracts rather than meetings.
Conclusion
Pact contract testing is the proven answer to E2E testing problems in distributed architectures. It doesn’t replace every form of testing, but it does eliminate most E2E tests (the ones that were breaking without adding value). For any team with 5+ microservices, introducing Pact pays its investment in the first quarter.
Frequently asked questions
Do I need a shared staging environment to run Pact contract tests?
No: consumer and provider verify in a decoupled way, each in its own CI. The consumer runs its test against a Pact mock and generates a JSON contract file that it publishes to the Pact Broker. The provider's pipeline downloads that contract, replays it against a running instance of the service and checks that every expectation is met. Each service tests in isolation in seconds, without giant docker-compose files.
What kinds of bugs does contract testing not catch?
Emergent business logic: if the bug lives in a five-service composition, such as a checkout flow, a two-party contract won't catch it. It also doesn't cover performance or concurrency, because contract tests are unit-scale and load testing still needs its own approach. The provider's internal schemas are out of scope too, since it only validates the interface with consumers. Where it shines is catching breaking changes, such as a renamed field, before deploy.
What is the Pact Broker for, and what are the alternatives?
The Broker is the hub between teams. The consumer's CI publishes the contract after its tests, the Broker versions it by commit or tag and notifies the provider, whose CI verifies and reports the result back. It also maintains a compatibility matrix of which provider and consumer versions work together, useful for deployment gates. You can use Pactflow as a managed broker or self-host with the pact-broker Docker image.