Provider Verification and Pact Broker

9 min read

The consumer test generates a contract and publishes it. This lesson covers the other half: how the provider downloads that contract, verifies its API satisfies every interaction, and how the Pact Broker ties the two halves together into a deployment safety net.

Provider verification test

The provider test is structurally different from a unit or component test — you do not write individual @Test methods for each interaction. Instead, Pact generates one test invocation per interaction at runtime by reading the Pact file. Your job is to start the real service, implement @State setup methods, and let Pact replay the interactions.

@Provider("UserService")
@PactBroker(
    url = "${PACT_BROKER_URL}",
    authentication = @PactBrokerAuth(token = "${PACT_BROKER_TOKEN}")
)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class UserServiceProviderPactTest {
 
    @LocalServerPort
    private int port;
 
    @Autowired
    private UserRepository userRepository;
 
    @BeforeEach
    void setUp(PactVerificationContext context) {
        context.setTarget(new HttpTestTarget("localhost", port));
    }
 
    @TestTemplate
    @ExtendWith(PactVerificationInvocationContextProvider.class)
    void verifyPact(PactVerificationContext context) {
        context.verifyInteraction();
    }
 
    @State("user 42 exists")
    void setupUser42() {
        userRepository.deleteAll();
        userRepository.save(new User(42L, "Alice", "alice@test.com"));
    }
 
    @State("user 999 does not exist")
    void ensureUser999Missing() {
        userRepository.deleteById(999L);
    }
}

What each piece does:

  • @Provider("UserService") — identifies this as the provider side of the UserService contract; must match the providerName in the consumer's @PactTestFor annotation exactly
  • @PactBroker(...) — tells Pact where to download consumer contracts from; reads the broker URL and authentication token from environment variables so credentials are never committed to source control
  • @SpringBootTest(RANDOM_PORT) — starts the real service on a random port; Pact replays each request against this live instance rather than a mock
  • @TestTemplate + PactVerificationInvocationContextProvider — Pact dynamically generates one test invocation per interaction in each downloaded Pact file; you never write these invocations manually
  • @State methods — set up the database state that matches the consumer's .given(...) clause before each interaction is replayed; the string value must be an exact match

What provider verification checks

When the test runs, Pact replays each request from the Pact file against the real running service and compares the response against the contract. The comparison uses the matchers recorded in the Pact file — not exact value equality.

The rule is: the provider may return additional fields, but must include every field the consumer defined at the specified type.

Consider a consumer contract that says { id: number, name: string }:

  • Provider returns { id: 1, name: "Alice", role: "admin", createdAt: "2024-01-01" }PASS — extra fields are allowed
  • Provider returns { id: 1, fullName: "Alice" }FAIL — the required name field is absent, and fullName was never declared in the contract

This asymmetry matters in practice. Provider teams can freely extend their responses with new fields. They cannot remove or rename fields that any consumer has declared in a published contract without first migrating those consumers.

Publishing verification results

After provider verification runs, results are published back to the Pact Broker automatically (configured via @PactBroker). The broker records: "UserService at version $SHA has verified OrderService's Pact file at version $SHA — result: passed." This record is what the can-i-deploy query reads.

The can-i-deploy command is added as a CI gate immediately before any deployment step:

# In UserService CI, before deploying to production:
pact-broker can-i-deploy \
  --pacticipant UserService \
  --version $GIT_SHA \
  --to-environment production

The output is binary: Computer says yes ✔ when the version being deployed is compatible with every consumer version already deployed to that environment, or Computer says no ✗ with a specific reason — for example, "OrderService version abc123 has not been verified against UserService version def456."

The Pact Broker

The broker is the shared registry that makes consumer-driven contract testing work across separate repositories and deployment pipelines.

  • Contract storage — holds every Pact file published by every consumer, versioned by Git SHA; old versions are retained so you can query historical compatibility
  • Verification results — records which provider version successfully verified which consumer contract version; without this record, can-i-deploy has no data to query
  • Dependency matrix — a table showing which version of each consumer is compatible with which version of each provider; the broker renders this as a visual grid in the UI
  • can-i-deploy — the key safety query: "can I deploy this version to this environment given the versions already deployed there?" It reads the dependency matrix and returns a pass/fail
  • Webhooks — trigger provider verification pipelines automatically when a consumer publishes a new Pact, so the provider team does not need to poll or coordinate manually

For hosting, you have two options. The self-hosted open-source broker is a Docker image you run yourself — free, covers the core features above. PactFlow is the managed hosted version, adding bi-directional contract testing (for teams that cannot modify the provider test), advanced analytics, a richer UI, and SLA-backed availability. For most teams starting out, the self-hosted broker is sufficient.

Handling breaking changes

The real test of contract testing is what happens when someone tries to make a breaking change. Walk through a concrete scenario: Order Service renames the field it reads from name to fullName. The developer updates the consumer test, runs it, generates a new Pact file, and publishes it to the broker. The new Pact file now requires fullName instead of name.

The provider's CI runs its verification test. User Service still returns name. The interaction for fullName fails. The broker records a failed verification. When the Order Service developer runs can-i-deploy for their new consumer version, the command returns no — because the provider version currently deployed to production has not verified the new Pact. The deployment is blocked.

Two resolution paths:

  1. Provider adds fullName alongside name (backward compatible). Provider deploys. Now both fields exist. Consumer can deploy. Provider later removes name once all consumers have migrated. This is the lower-risk path and the one the Pact workflow is designed to support.
  2. Both teams coordinate a simultaneous deploy window. This requires locking deployment pipelines across two services, increases blast radius, and is harder to roll back. Avoid it when option 1 is possible.

For how this fits into a full deployment pipeline with environment promotion gates, cross-reference the CI/CD for QA course.

Pact Broker
  • – Pact file + Git SHA
  • – Triggered by CI
  • – All interactions captured
  • – Downloads Pact files
  • – Runs @State setup
  • – Publishes results
  • – Checks compatibility matrix
  • – CI gate before deploy
  • – Env-aware versioning
  • All consumer versions –
  • All provider versions –
  • Compatibility record –

⚠️ Common mistakes

  • Not implementing @State methods for every provider state the consumer defines. If Order Service's Pact says "given user 42 exists" but the provider test has no @State("user 42 exists") method, the verification will fail with a 404 because the user does not exist in the test database when Pact replays the request. Every .given(...) string in the consumer test needs a matching @State method on the provider side — the strings must match exactly, including casing.
  • Running provider verification against a stub or mock instead of the real running service. The entire value of provider verification is that it runs against real code. Running against a mock proves only that a mock works. Use @SpringBootTest(RANDOM_PORT) so the full application context — including your controllers, service layer, and database queries — executes during verification.
  • Ignoring can-i-deploy results in CI. Teams often add Pact testing without adding the can-i-deploy gate. Without the gate, contracts are verified but deployments are not blocked when verifications fail. The feedback loop exists but is not enforced. Add the command as a required CI step — treat a no answer the same way you would treat a failing test suite.

🎯 Practice task

  1. Add the Pact provider JUnit 5 dependency to your User Service project. Write the @Provider test class shown above. Add a @State("user 42 exists") method that inserts a test user via the repository. Confirm the class compiles.
  2. Run the provider test locally — you will get a connection error because there is no broker yet. Replace @PactBroker with @PactFolder("../order-service/target/pacts") to load the Pact file directly from the consumer's output directory. Re-run and confirm verification attempts to execute.
  3. Make the verification pass end-to-end. Then deliberately break it: rename the name field in User Service's JSON response to fullName. Run the provider test again. Read the failure message and identify exactly what mismatch it reports and which interaction failed.
  4. Restore the name field and add a new @State("user 999 does not exist") method. Add the corresponding 404 interaction to the consumer Pact and re-run both the consumer and provider tests to confirm both pass.
  5. Read the can-i-deploy documentation at docs.pact.io. Write the exact pact-broker can-i-deploy command you would add as a GitHub Actions workflow step to gate deployment of UserService to production, using ${{ github.sha }} as the version.

With consumer tests generating contracts and provider verification enforcing them, you now have a complete contract-testing loop. The next lesson extends this foundation to asynchronous messaging contracts — where services communicate via events rather than HTTP.

// tip to track lessons you complete and pick up where you left off across devices.