You’re about to deploy your payment service. Your tests pass. Code review looks good. But three other teams depend on your API, and you’re not entirely sure if your changes will break their code. Sound familiar?
This is the microservices testing dilemma. Integration tests are slow and brittle. End-to-end tests are worse. Mocking feels safe until production proves otherwise. You end up either deploying nervously or coordinating release schedules across teams, defeating the whole point of microservices.
Contract testing offers a way out. It’s not perfect, but it’s practical. Here’s how it actually works and why teams dealing with real-world Java microservices are adopting it.
The Problem: Testing Between Services
Let’s say you work on the Order Service. It calls the Inventory Service to check stock levels. Your colleague Sarah maintains the Inventory Service. She’s about to rename a field from quantity to availableQuantity because it’s clearer.
Your perspective: You wrote tests that mock the Inventory Service returning quantity. They all pass. You have no idea Sarah’s about to break your production deployment.
Sarah’s perspective: She updated her service, her tests pass, and she deploys Friday afternoon. Her service works perfectly—from her point of view.
Saturday morning: Orders start failing. Customers can’t buy anything. Your on-call phone rings. You discover the field rename broke the integration. Now you’re writing hotfixes while your coffee gets cold.
This happens because traditional testing approaches have blind spots. Unit tests mock away the integration. End-to-end tests are too slow to run on every commit and too fragile to maintain. You need something in between.
What Contract Testing Actually Does
A contract is a formal agreement about how two services communicate. The consumer (Order Service) says “I expect the provider (Inventory Service) to respond with these fields in this format.” The provider says “I promise to deliver that format.”
Contract testing verifies both sides honor their promises independently. The Order Service tests that it can handle the responses it expects. The Inventory Service tests that it actually returns what consumers expect. If either side breaks the contract, tests fail before deployment.
The clever bit: these tests run independently in each service’s pipeline. No coordination required. No shared test environments. Each team knows immediately if they’re about to break someone else.
Pact: Making Contracts Real
Pact is the most popular contract testing framework, and it works particularly well with Java microservices. It has two main pieces: consumer tests and provider tests.
Consumer tests define what you expect from the provider and generate a pact file—a JSON document describing the contract.
Provider tests take that pact file and verify the real provider can fulfill it.
Let’s see this with real code.
A Real Example: Order and Inventory Services
Imagine the Order Service needs to check inventory before placing an order. Here’s what the actual interaction looks like:
Request: GET /inventory/items/SKU-12345
Expected Response:
{
"sku": "SKU-12345",
"quantity": 42,
"reserved": 5
}
Writing the Consumer Test
In your Order Service, you write a Pact consumer test. This runs during your normal test suite—no external dependencies:
@ExtendWith(PactConsumerTestExt.class)
public class InventoryServiceContractTest {
@Pact(consumer = "order-service", provider = "inventory-service")
public RequestResponsePact checkInventoryPact(PactDslWithProvider builder) {
return builder
.given("item SKU-12345 exists with stock")
.uponReceiving("a request for inventory")
.path("/inventory/items/SKU-12345")
.method("GET")
.willRespondWith()
.status(200)
.headers(Map.of("Content-Type", "application/json"))
.body(new PactDslJsonBody()
.stringType("sku", "SKU-12345")
.integerType("quantity", 42)
.integerType("reserved", 5))
.toPact();
}
@Test
@PactTestFor(pactMethod = "checkInventoryPact")
void testInventoryCheck(MockServer mockServer) {
// Your real client code that calls inventory service
InventoryClient client = new InventoryClient(mockServer.getUrl());
InventoryItem item = client.checkInventory("SKU-12345");
assertThat(item.getSku()).isEqualTo("SKU-12345");
assertThat(item.getQuantity()).isGreaterThan(0);
// You're testing YOUR code can handle the response
}
}
This test does two things. First, it defines what you expect from the Inventory Service—the contract. Second, it verifies your client code can actually handle that response format.
Notice the stringType and integerType methods. These use flexible matching. You’re saying “I expect a string for SKU and integers for quantities” without demanding exact values. This prevents brittle tests while ensuring structure is correct.
When this test runs, Pact generates a JSON file: order-service-inventory-service.json. This is the contract. You commit it to your repo or publish it to a Pact Broker.
The Provider Test
Now Sarah, who maintains the Inventory Service, writes a provider test. This takes your contract and verifies her service fulfills it:
@Provider("inventory-service")
@PactBroker(url = "https://pact-broker.yourcompany.com")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class InventoryServiceProviderTest {
@LocalServerPort
private int port;
@Autowired
private InventoryRepository repository;
@BeforeEach
void setup(PactVerificationContext context) {
context.setTarget(new HttpTestTarget("localhost", port));
}
@State("item SKU-12345 exists with stock")
void itemExists() {
// Set up test data that matches the contract state
repository.save(new InventoryItem("SKU-12345", 42, 5));
}
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerificationTest(PactVerificationContext context) {
context.verifyInteraction();
}
}
This test starts Sarah’s real Spring Boot application and replays the request from your contract. If her service returns the expected format, the test passes. If she renames quantity to availableQuantity, this test fails immediately—before she deploys.
The @State annotation is crucial. It tells Pact “before running this interaction, set up this specific scenario.” This lets Sarah create the exact conditions your contract expects without maintaining a permanent test database.
The Pact Broker: Connecting the Dots
You’ve got consumer tests generating contracts and provider tests verifying them. How do they find each other, especially across team boundaries?
Enter the Pact Broker. It’s a service that stores contracts and tracks which versions of consumers are compatible with which versions of providers.
When your Order Service pipeline runs, it publishes the contract:
mvn pact:publish \ -Dpact.broker.url=https://pact-broker.yourcompany.com \ -Dpact.consumer.version=1.4.2 \ -Dpact.consumer.tags=main
When Sarah’s Inventory Service pipeline runs, it fetches all contracts from consumers and verifies against them:
mvn pact:verify \ -Dpact.broker.url=https://pact-broker.yourcompany.com \ -Dpact.provider.version=2.1.0
The broker keeps track of compatibility. You can query it: “Can I safely deploy Order Service version 1.4.2 to production given the Inventory Service version running there?” It answers definitively because it knows which contracts are verified.
Handling Real-World Messiness
Clean examples are great, but production is messier. Here’s how to handle common complications.
When the Provider Changes
Sarah wants to add a new field lastRestocked to the inventory response. She adds it and runs her provider tests. They still pass—adding fields doesn’t break existing consumers. She deploys confidently.
You don’t care about lastRestocked yet, so your code ignores it. Everything works. This is backwards compatibility in action.
Now Sarah wants to remove the reserved field. She tries, and the provider test fails because your contract still expects it. She has two choices:
Option 1: Coordinate with you to remove the field from your contract first, then remove it from her service. This requires communication but is sometimes necessary.
Option 2: Mark the field as deprecated, add a new field that replaces it, and eventually remove the old one after all consumers migrate. This is slower but more decoupled.
When Multiple Consumers Exist
The Inventory Service isn’t just used by Order Service. Warehouse Service and Reporting Service also depend on it. Each publishes their own contract.
Sarah’s provider tests verify against all three contracts simultaneously. If a change breaks any consumer, she knows before deploying. This is powerful—she maintains backwards compatibility with all consumers without manually coordinating with three teams.
Handling Authentication
Your real services probably require auth tokens. How do you test that?
@State("item SKU-12345 exists with stock")
void itemExists(Map<String, Object> params) {
// Set up test data
repository.save(new InventoryItem("SKU-12345", 42, 5));
// Configure test auth that accepts any token
testAuthConfig.allowAllTokens();
}
In provider tests, you typically bypass real authentication or use test credentials. You’re testing the business logic and data format, not your auth implementation. Keep provider tests focused on the contract.
Versioning Services
You can run provider tests against specific consumer versions:
@PactBroker(
url = "https://pact-broker.yourcompany.com",
consumerVersionSelectors = {
@ConsumerVersionSelector(tag = "main"),
@ConsumerVersionSelector(tag = "production")
}
)
This ensures you don’t break what’s currently in production OR what’s about to be deployed. It’s a safety net for both current and upcoming releases.
What Contract Testing Doesn’t Cover
Pact isn’t magic. It solves specific problems and ignores others by design.
It doesn’t test business logic. If your inventory check should reject orders when quantity equals reserved (no available stock), but your contract just checks field presence, Pact won’t catch that bug. You still need unit tests.
It doesn’t test performance. Pact verifies response format, not response time. If the Inventory Service starts taking 10 seconds to respond, contract tests pass happily while your production system burns.
It doesn’t test sequence or state. If you need to call three endpoints in a specific order, contract testing verifies each call individually but not the orchestration. You might still need integration tests for complex workflows.
It doesn’t catch every breaking change. If Sarah changes quantity from an integer to a string, Pact catches it. If she changes the business rules about when items are “in stock,” Pact might not. It’s focused on technical contracts, not business semantics.
Integrating with Your Pipeline
Contract testing only works if it’s automated. Here’s a practical CI/CD flow:
Consumer side (Order Service):
- Developer pushes code
- Pipeline runs all tests including Pact consumer tests
- If tests pass, publish contracts to Pact Broker with git commit hash
- Before deploying, check “can-i-deploy” with Pact Broker
- Deploy only if all providers can satisfy this consumer’s contracts
Provider side (Inventory Service):
- Developer pushes code
- Pipeline runs all tests including Pact provider verification
- If verification passes, mark this provider version as verified in Pact Broker
- Before deploying, check “can-i-deploy” to ensure no consumers depend on old behavior
- Deploy
The “can-i-deploy” check is critical. It’s a command that queries the Pact Broker:
pact-broker can-i-deploy \ --pacticipant order-service \ --version 1.4.2 \ --to-environment production
It returns success only if all provider contracts are verified. This automated gate prevents broken deployments.
Making It Work Across Teams
The technical setup is straightforward. The organizational part is harder.
Someone needs to own the Pact Broker. It’s infrastructure that all teams depend on. Platform or DevOps teams usually run it. Set it up once, but make sure it’s monitored and backed up.
Contracts are code. Treat them like APIs. Breaking contracts breaks consumers. This requires discipline. Some teams make contract files require approval from team leads before merging.
Communication still matters. Contract testing reduces coordination but doesn’t eliminate it. When you need to make breaking changes, you still talk to consuming teams. The difference is you can validate the changes work before production.
Start with one pair of services. Don’t try to contract-test everything immediately. Pick a consumer and provider that frequently have integration issues. Get contract testing working there, learn the patterns, then expand.
Common Pitfalls and How to Avoid Them
Over-specific contracts. Don’t match exact values unless they’re truly fixed. Use stringType instead of exact strings, integerType instead of specific numbers. Contracts should be flexible enough to allow provider evolution.
Under-tested edge cases. Your consumer should define contracts for error scenarios too. What happens when the item doesn’t exist? When inventory is zero? Write contracts for 404s, 400s, and 500s.
Forgetting about state setup. Provider tests fail randomly because test data isn’t set up correctly. Take time to implement @State methods properly. They’re as important as the verification logic.
Publishing unverified contracts. Only publish contracts from your main branch or release branches. Don’t publish from feature branches—that pollutes the Pact Broker with experimental contracts that might never be deployed.
Ignoring the can-i-deploy check. It’s tempting to skip this when you’re in a hurry. Don’t. That’s when you need it most. Make it a required step in your pipeline.
Tools and Alternatives
Pact JVM is the Java implementation we’ve been discussing. It integrates nicely with JUnit, Spring Boot, and Maven/Gradle.
Spring Cloud Contract is an alternative from the Spring team. It’s more opinionated and tightly integrated with Spring Boot. Some teams prefer it if they’re already heavily invested in Spring ecosystem.
Postman Contract Testing works if you’re already using Postman for API testing. It’s less sophisticated than Pact but easier to adopt if your team is already comfortable with Postman.
GraphQL has its own approaches with schema stitching and federation that provide some contract guarantees. If you’re using GraphQL, you might not need Pact.
Real Team Experiences
At a fintech company I worked with, they had eight teams maintaining 24 microservices. Integration issues happened weekly. Someone would deploy, something else would break, and they’d spend hours figuring out which change caused it.
They started with Pact between their most troublesome pair: the loan origination service and the credit check service. Within a month, integration breaks between those two services dropped to zero. Not reduced—eliminated.
They expanded gradually. After six months, all 24 services had contracts. Deployment confidence went up. Deploy frequency increased because teams weren’t afraid of breaking things. The on-call burden decreased because fewer production issues originated from integration problems.
The investment was significant—about two weeks of engineering time per service to set up tests and integrate with CI/CD. But they calculated that just three prevented production incidents paid for the entire effort.
The Bottom Line
Contract testing with Pact won’t solve all your testing problems. You still need unit tests for logic, integration tests for workflows, and monitoring for production issues.
What it does solve is the coordination problem. It lets teams deploy independently while maintaining confidence they won’t break other services. That’s valuable when you have multiple teams, frequent deployments, and the need to move fast without breaking things.
The setup requires investment. You need a Pact Broker, CI/CD integration, and team buy-in. But once it’s working, it pays dividends every time someone deploys without breaking production.
Start small, prove value with one critical integration, then expand. Don’t aim for 100% contract coverage immediately—that’s a recipe for frustration. Focus on the integrations that cause the most problems and work outward from there.
Useful Resources
- Pact JVM Documentation
https://docs.pact.io/implementation_guides/jvm - Pact Broker Setup Guide
https://docs.pact.io/pact_broker - Pactflow (Managed Pact Broker)
https://pactflow.io/ - Spring Cloud Contract
https://spring.io/projects/spring-cloud-contract - Contract Testing Best Practices
https://docs.pact.io/getting_started/best_practices - Pact Workshop (Java)
https://github.com/pact-foundation/pact-workshop-jvm-spring - Can I Deploy Tool
https://docs.pact.io/pact_broker/can_i_deploy - Martin Fowler on Contract Testing
https://martinfowler.com/bliki/ContractTest.html
Thank you!
We will contact you soon.
Eleftheria DrosopoulouOctober 28th, 2025Last Updated: October 22nd, 2025

This site uses Akismet to reduce spam. Learn how your comment data is processed.