Contract Testing with Pact for Microservices
Actualizado: 2026-05-03
Any organisation that has migrated to microservices eventually hits 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 — and its most popular implementation, Pact[1] — solves the problem by moving it to a different layer.
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
endThe 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: “which provider version works with which consumer version?” — 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.