Introduction to Pact

9 min read

Pact is the dominant consumer-driven contract testing tool. It is open-source, battle-tested at companies like REA Group and Atlassian, and supports Java, JavaScript, .NET, Ruby, Go, Python, and more. The previous lesson explained the contract testing concept; this lesson covers Pact's core vocabulary, the end-to-end workflow, and the Pact file format in detail — so you understand exactly what the tool generates and why before you write your first test in the next lesson.

The four Pact concepts

Pact has four terms that appear everywhere in its documentation and tooling. It is worth internalising them before touching any code.

Consumer is the service that calls another service's API. In a system where Order Service retrieves user details from User Service, Order Service is the consumer. The consumer team owns the contract and is responsible for keeping it up to date with what their service actually needs.

Provider is the service that exposes the API being consumed. User Service is the provider. The provider team is responsible for running verification and publishing the results back to the broker. They do not write the contract — they verify that their service satisfies it.

Pact file is a JSON document generated by the consumer test. It records every interaction the consumer declared: the HTTP method, path, and request headers the consumer sends, and the response status, headers, body shape, and matching rules the consumer expects back. The file is named ConsumerName-ProviderName.json and lives in the target/pacts/ directory after the consumer tests run.

Pact Broker is a hosted service — either self-hosted or PactFlow cloud — where Pact files are stored, versioned, and queried. It is the shared source of truth between consumer teams and provider teams. Without a broker you would have to copy Pact files between repositories manually, which breaks the versioning and can-i-deploy features that make Pact valuable at scale.

The Pact workflow end to end

The workflow has four distinct phases that happen in different pipelines at different times.

Phase 1 — Consumer tests. A developer on the Order Service team writes a test using the Pact JUnit 5 library. The test runs against a Pact mock server — not a real User Service. The developer tells the mock server: "When you receive GET /users/1, respond with a 200 and a body containing at least id, name, and email." The consumer code runs, calling the mock server as if it were User Service. After the test passes, the Pact library writes the interaction to target/pacts/OrderService-UserService.json.

Phase 2 — Publish. After the consumer CI pipeline passes its build and unit tests, a publish step uploads the generated Pact file to the Pact Broker. The file is tagged with the consumer service name, the provider service name, and the Git commit SHA. This tagging is what makes can-i-deploy work — without version information the broker cannot answer compatibility questions.

Phase 3 — Provider verification. When the User Service CI pipeline runs, it downloads every Pact file from the broker that names User Service as the provider. The pipeline starts the real User Service process in-process (no Docker, no network) and replays each interaction from the Pact file against it. It checks whether the actual response satisfies the matchers declared in the contract. The verification results — pass or fail, per interaction — are published back to the broker.

Phase 4 — can-i-deploy. Before User Service or Order Service deploys to any environment, the CI pipeline runs a single command that queries the broker: "Is version $SHA of UserService compatible with every consumer version currently deployed to production?" If every relevant contract is verified as passing, the answer is yes and the deployment proceeds. If any contract is broken or unverified, the answer is no and the pipeline stops. This gate prevents deployments that would break a running consumer.

The Pact file format

Understanding the JSON structure makes debugging and reviewing contracts much easier. Here is the file generated after a single consumer interaction test:

{
  "consumer": { "name": "OrderService" },
  "provider": { "name": "UserService" },
  "interactions": [
    {
      "description": "a request for user 1",
      "providerStates": [{ "name": "user 1 exists" }],
      "request": {
        "method": "GET",
        "path": "/users/1"
      },
      "response": {
        "status": 200,
        "headers": { "Content-Type": "application/json" },
        "body": {
          "id": 1,
          "name": "Alice",
          "email": "alice@test.com"
        },
        "matchingRules": {
          "body": {
            "$.id": { "matchers": [{ "match": "type" }] },
            "$.name": { "matchers": [{ "match": "type" }] },
            "$.email": { "matchers": [{ "match": "regex", "regex": ".+@.+\\..+" }] }
          }
        }
      }
    }
  ]
}

The providerStates array maps to provider state handlers you write on the provider side — they seed the database before Pact replays the interaction. The body object holds example values that the mock server returns to the consumer during consumer tests. The matchingRules are what the provider verification step actually checks: $.id must be a type match (any number is fine, not just 1), $.name must be a type match (any string), and $.email must satisfy the regex. This flexibility is what makes contracts durable — provider teams can change test data, rotate IDs, and rename internal values without breaking the contract, as long as the structure and types stay correct.

Maven setup

Add two dependencies: one for writing consumer tests, one for running provider verification. Both are in the au.com.dius.pact group and use the same version.

<!-- Consumer test dependency -->
<dependency>
    <groupId>au.com.dius.pact.consumer</groupId>
    <artifactId>junit5</artifactId>
    <version>4.6.5</version>
    <scope>test</scope>
</dependency>
 
<!-- Provider verification dependency -->
<dependency>
    <groupId>au.com.dius.pact.provider</groupId>
    <artifactId>junit5</artifactId>
    <version>4.6.5</version>
    <scope>test</scope>
</dependency>

The consumer dependency lives in the Order Service repository. The provider dependency lives in the User Service repository. You never need both in the same project — that would mean a service is testing itself as both consumer and provider, which defeats the purpose.

The scope of a Pact test

This is the most important concept to internalise before writing your first test: Pact tests are not about full API coverage. They represent the consumer's perspective only. If Order Service reads id, name, and email from User Service's response and ignores everything else, the contract mentions exactly those three fields. The role, createdAt, lastLogin, and preferences fields are not in the contract.

This is intentional. User Service is free to add new fields, rename fields the consumer does not use, restructure its internal response format, or change the type of lastLogin from a timestamp to a formatted string — none of that breaks the contract. The consumer team will never know it happened, and they will never need to. Teams stay independent. The contract surface area shrinks to what genuinely matters.

Step 1 of 5

Consumer writes test

Order Service developer writes a Pact test against a mock server, defining the exact request and the minimum response fields the service needs.

⚠️ Common mistakes

  • Using Pact to test happy paths and error paths from the provider's perspective. Pact tests the structural contract between a specific consumer and a specific provider — not every possible response the API can return. Error handling belongs in provider component tests where you control the conditions that trigger errors. A Pact file full of error-path interactions inflates the contract surface area and slows down provider verification without adding meaningful compatibility guarantees.
  • Not tagging Pact files with the Git SHA when publishing. Without version information the broker stores the contract but cannot answer can-i-deploy questions meaningfully. Always publish with --consumer-app-version=$GIT_SHA (or the equivalent in your CI environment). Without this, the can-i-deploy feature is disabled and you lose the primary safety gate that makes Pact valuable in a multi-service deployment pipeline.
  • Treating the Pact file as human-written documentation. Pact files are generated by the consumer test and must never be hand-edited. The trust relationship in Pact depends on the file being an accurate reflection of what the consumer code actually exercises. If you edit the JSON directly, the contract no longer represents what the consumer test proves, and provider verification loses its meaning.

🎯 Practice task

  1. Add the Pact JUnit 5 consumer dependency to a test project. Verify the dependency resolves by running mvn dependency:tree | grep pact. Confirm you see au.com.dius.pact.consumer:junit5 in the output — if the version is not resolving, check Maven Central for the latest 4.6.x release.
  2. Study the sample Pact JSON above and identify: which field uses a type matcher, which uses a regex, and what the matching rule for $.name would accept and reject. Then reason through this: what would happen during provider verification if the provider returned "name": 42 (a number instead of a string)?
  3. Read the Pact Broker's can-i-deploy documentation at docs.pact.io. Identify the three pieces of information the command needs to answer the deployment question — the pacticipant name, the pacticipant version, and the target environment — and explain why each matters for a correct compatibility decision.
  4. Sketch the Pact file that would be generated if Order Service's consumer test defines: "When I call GET /orders/1, I expect status 200, body with orderId (string), status (string matching one of PENDING, CONFIRMED, CANCELLED), total (decimal number)." Write out the matchingRules block manually — what matcher type would you use for each field?
  5. Research PactFlow at pactflow.io — the hosted Pact Broker. Identify two features it provides beyond a basic self-hosted Pact Broker that would matter for a team managing 10 services: consider the bi-directional contract testing support and the team-level access controls as starting points.

The next lesson moves from concept to code: you will write a complete Pact consumer test for Order Service, run it against the mock server, and inspect the generated Pact file.

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