I remember a Friday afternoon when a seemingly harmless deployment of the Product Service broke the Order Service in production. The Product team had added a new field to their API response, but the Order Service was still expecting the old structure. We found out only after customers started seeing error 500s. That pain pushed me to look for a better way. I wanted my services to evolve independently without constant integration test suites that took hours and required both services running. That search led me to Consumer-Driven Contract Testing with Pact.
Consumer-Driven Contract Testing flips the traditional testing pyramid. Instead of the provider dictating the API and the consumer hoping it stays stable, the consumer defines exactly what it expects. This contract is then shared with the provider, which verifies it independently. No end-to-end tests, no brittle orchestration. Just a promise that the provider will honor the consumer’s needs. It sounds simple, but the impact on release cycles is enormous.
Let me walk you through how I set this up between two Spring Boot services: an Order Service (consumer) and a Product Service (provider). I will include code so you can see exactly how it works.
First, I added Pact JVM dependencies to the consumer’s pom.xml. For JUnit 5, the consumer-side library is au.com.dius.pact.consumer:junit5. I also included the Pact Maven plugin for publishing contracts to a Pact Broker. The provider side needs au.com.dius.pact.provider:junit5spring and Spring’s mock MVC support.
Now, the consumer test. In the Order Service, I wrote a test that defines the expected interaction with the Product Service. I used @PactTestFor and @Pact annotation to define the contract. Here is how the test looked:
@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "ProductService", port = "8080")
public class ProductClientPactTest {
@Pact(consumer = "OrderService")
public V4Pact createProductPact(PactDslWithProvider builder) {
return builder
.given("a product with ID 1 exists")
.uponReceiving("a request for product ID 1")
.path("/api/v1/products/1")
.method("GET")
.willRespondWith()
.status(200)
.headers(Map.of("Content-Type", "application/json"))
.body(new PactDslJsonBody()
.integerType("id", 1)
.stringType("name", "Widget")
.decimalType("price", 19.99)
.stringType("status", "AVAILABLE")
.integerType("stockQuantity", 100))
.toPact(V4Pact.class);
}
@Test
@PactTestFor(pactMethod = "createProductPact")
public void testGetProductById(MockServer mockServer) {
var client = new ProductClient(new RestTemplate(), mockServer.getUrl());
ProductDto product = client.getProductById(1L);
assertEquals("Widget", product.getName());
assertEquals(new BigDecimal("19.99"), product.getPrice());
}
}
This test does two things: it records the contract and validates that my client code works correctly against a mock server. The contract is stored as a JSON file in target/pacts. I then pushed that contract to a Pact Broker using the plugin: mvn pact:publish.
But why a broker? Because contracts need to be shared across teams and versions. The broker holds the truth and can show which consumer versions are compatible with which provider versions. Have you ever wanted to see, at a glance, whether the latest Product Service commit will break any consumer? The broker gives you that matrix.
Now the provider side. In the Product Service, I added a verification test that reads the contract from the broker and checks whether the actual API matches it. With Spring Boot, I used @Provider("ProductService") and @PactBroker(url = "http://localhost:9292"). Here is a simplified test:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Provider("ProductService")
@PactBroker(url = "http://localhost:9292")
public class ProductServicePactVerificationTest {
@LocalServerPort
int port;
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
context.verifyInteraction();
}
@BeforeEach
void setUp(PactVerificationContext context) {
context.setTarget(new HttpTestTarget("localhost", port));
}
}
The test runs automatically when I run mvn test on the provider side. It fetches all contracts from the broker that target the ProductService and verifies each interaction. If the Product Service changes a field type or removes an endpoint, the test fails. No more surprises.
I also added provider states using the @State annotation to set up preconditions. For the “product exists” state, I seeded a test database. That way the contracts can specify different states like “product not found” or “no products in catalog”. Pact will match the state description during verification.
Now, what happens when a contract changes? The consumer modifies its test, publishes a new contract, and the provider verifies it – ideally in a CI pipeline. The Pact Broker also supports webhooks to trigger provider builds automatically when a contract changes. This creates a feedback loop: if a change would break the provider, the provider team knows immediately, before merging.
Some might ask: is this better than Spring Cloud Contract? Both are valid, but Pact has a larger ecosystem with client libraries for many languages and a mature broker. Spring Cloud Contract works well if you are all-in on Spring, but Pact integrates across language boundaries, which matters if your architecture includes Node.js or Python services.
Of course, contract testing is not a silver bullet. It tests only the API contract, not the full behavior. You still need unit tests for business logic and perhaps a few smoke tests in production. But it eliminates the need for most end-to-end tests, which are slow, flaky, and couple deployments.
Let me share a personal observation: after adopting Pact, our deployment frequency doubled. The Order Service could release without waiting for the Product Service to be ready. We caught incompatibilities within minutes instead of hours. The mental shift from “integration testing” to “contract verification” was liberating.
If you are building microservices, give contract testing a try. Start small: pick one consumer-provider pair. Write a Pact test on the consumer, then verify on the provider. Use a broker to share contracts. Automate the verification in CI. You will notice the difference.
Now, if you found this guide helpful, please like, share, and comment below. What has been your experience with contract testing? Have you tried Pact or Spring Cloud Contract? Let me know. And if you want more articles like this, subscribe to the newsletter. Thank you for reading.
As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva