I was building a microservices system for an e-commerce platform. Everything worked perfectly in isolation. The order service passed all its unit tests. The product service passed all of its. But the first time a real order was placed? The system failed. The order service expected a field called productName, but the product service returned name. This tiny mismatch caused a major outage. It hit me then: we can’t catch these integration bugs by looking inward at a single service. We need a different way.
That experience led me down the path of contract testing. Specifically, a method called consumer-driven contract testing. It’s a simple but powerful idea. The service making a request, the consumer, defines a formal “contract.” This contract states exactly what it expects from the service providing the data, the provider. The provider then verifies it meets these expectations. This happens before any code reaches production.
Think about it. How many times have you seen a bug that only appears when two services talk to each other? You deploy an update, and a seemingly unrelated part of the system breaks. This is the problem contract testing solves.
This is where a tool like Pact shines. It automates this contract process. The consumer writes a test that defines the expected request and response. Pact turns this into a shareable contract file. The provider then runs its own tests against this contract to prove it complies. It’s like a handshake agreement, but automated and enforceable.
Let’s look at how this works in practice with a Spring Boot service. Imagine our order service needs product details. First, we set up a client to call the product service.
@Component
public class ProductServiceClient {
private final RestClient restClient;
public ProductServiceClient(RestClient.Builder builder) {
this.restClient = builder
.baseUrl("http://product-service")
.build();
}
public ProductResponse fetchProduct(Long id) {
return restClient.get()
.uri("/products/{id}", id)
.retrieve()
.body(ProductResponse.class);
}
}
This client is simple. But how do we know the remote service will answer correctly? This is where we write a Pact test from the consumer’s point of view. We’re not testing the real product service here. We’re defining what we expect from it.
@PactTestFor(providerName = "ProductService")
public class ProductClientContractTest {
@Pact(consumer = "OrderService")
public RequestResponsePact validProductRequest(PactDslWithProvider builder) {
return builder
.given("product with ID 123 exists")
.uponReceiving("a request for product 123")
.path("/products/123")
.method("GET")
.willRespondWith()
.status(200)
.body(new PactDslJsonBody()
.numberType("id", 123L)
.stringType("name", "Wireless Mouse")
.numberType("price", 29.99)
)
.toPact();
}
@Test
@PactTestFor(pactMethod = "validProductRequest")
void testProductExists(MockServer mockServer) {
// This test uses the mock server defined by the Pact
ProductServiceClient client = new Client(mockServer.getUrl());
ProductResponse product = client.fetchProduct(123L);
assertThat(product.name()).isEqualTo("Wireless Mouse");
}
}
Run this test, and Pact generates a JSON contract file. It contains the promise: “If you receive a GET request for /products/123, you must respond with this specific JSON structure.” This file is the single source of truth.
Now, here’s a crucial question: what happens when the provider, the product service team, changes their API? The contract is published to a shared server called a Pact Broker. The provider’s build pipeline pulls this contract and runs a verification test.
The provider side test looks like this. It starts its actual Spring Boot application and tells Pact to replay all the requests from the contract against the real running service.
@Provider("ProductService")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ProductServiceProviderTest {
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void verifyPact(PactVerificationContext context) {
context.verifyInteraction();
}
@BeforeEach
void setup(PactVerificationContext context) {
context.setTarget(new HttpTestTarget("localhost", port));
}
@State("product with ID 123 exists")
public void setupProduct123() {
// Setup test data in your database
productRepository.save(new Product(123L, "Wireless Mouse", 29.99));
}
}
If the product service returns a different field name or omits a required field, this verification test fails. It fails immediately in the provider’s CI pipeline, blocking the change. The team is alerted to the breaking change before it reaches production. They can then talk to the order service team and decide on the right fix.
What about more complex scenarios? Let’s say a field is optional. Pact handles this elegantly. In your consumer test, you use matchers like optionalString().
.willRespondWith()
.body(new PactDslJsonBody()
.numberType("id", 123L)
.stringType("name", "Wireless Mouse")
.optionalString("description", "An ergonomic mouse") // This field can be null or missing
)
The provider isn’t forced to always include a description. It can choose to omit it, and the contract still passes. This flexibility is key for real-world APIs.
Adopting this method changes team dynamics. It fosters communication. The consumer drives the API design based on its needs, not the other way around. It turns integration from a guessing game into a documented, testable agreement. The Pact Broker gives everyone visibility into who depends on what.
Are you tired of the “works on my machine” syndrome turning into “broke in production”? This approach moves those integration discussions left, to the earliest possible moment: during development and testing.
Integrating this into your CI/CD pipeline is straightforward. Your consumer build generates and publishes pacts. Your provider build downloads and verifies them. A GitHub Actions workflow step for the provider might look like this:
- name: Verify Pacts
run: ./mvnw test -Dpact.provider.version=${{ github.sha }} -Dpact.verifier.publishResults=true
env:
PACT_BROKER_URL: ${{ secrets.PACT_BROKER_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
The result is a safety net. You gain confidence that your independent services will work together. You can refactor or add features without the constant fear of silently breaking a distant, dependent service.
So, the next time you design a service interaction, ask yourself: is this agreement documented in code? Can it be tested automatically? If not, you might be setting yourself up for a late-night debugging session. Start with a simple interaction, define its Pact, and see how it transforms your deployment confidence. The initial setup pays for itself the first time it catches a breaking change.
Have you tried contract testing before? What was the biggest integration surprise you’ve encountered in a distributed system? I’d love to hear your stories. If you found this walkthrough helpful, please share it with your team or leave a comment below discussing your approach. Let’s build more resilient systems together.
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