π Real-World Wake-Up Call
Once upon a time at work π, I was working on a project that heavily relied on multiple upstream services owned by different teams to process order data. One seemingly ordinary day, one of those services made a change to their API β they updated the productDescription field from a String to a List.
Our service, still expecting a String, started throwing 400 Bad Request errors out of nowhere. This seemingly small change caused unexpected failures and impacted our downstream processing.
This incident made it painfully clear: we needed a better way to catch breaking changes in API contracts early, before they hit production. Thatβs when we started exploring Contract Testing as a solution. Among the tools we considered were Spring Cloud Contract and Pact.
Here url for POC we created: https://github.com/levi-a07/contract-testing
Spring Cloud Contract is a testing framework that helps ensure reliable communication between microservices by using contract testing
A contract is a written agreement (in code) that defines:
- What the producer (API provider) promises to return, based on what the consumer (API client) sends.
Spring Cloud Contract allows you to:
- Write these contracts (in Groovy/YAML),
- Automatically generate tests for the producer,
- Automatically generate mock stubs for the consumer.
Sample looks like:
description: Should return order by id
request:
method: GET
url: /orders/1
response:
status: 200
headers:
Content-Type: application/json
body:
orderId: "1"
productId: "product-1234"
quantity: 2
price: 29.99
matchers:
body:
- path: $.orderId
type: by_regex
value: "\\d+" # Matches any numeric orderId
- path: $.productId
type: by_regex
value: "product-\\d+" # Matches a product ID like product-123
- path: $.quantity
type: by_type
value: number # Matches any numeric quantity
- path: $.price
type: by_type
value: number # Matches any numeric price
Types of Contract Testing
There are 3 type of contracts:
1. Consumer-Driven Contracts (CDC)
The contract is usually written from the consumerβs point of view, describing what it expects from the producer. The consumer service then uses a stub β an auto-generated mock server based on the contract β to simulate the producerβs behavior during testing.
This stub reflects the contract exactly, not the actual producer code. The consumer team can share the contract via a pull request to the producer repo or through a shared location. If the producer later changes the contract, the stub updates accordingly, and any mismatch will cause the consumer's test to fail, alerting the team to the breaking change early.
Based on the contract, Automatic Junit tests are created on Producer side.
The generated tests simulate requests described in the contract and verify that the producer's real implementation returns the expected response. This way we catch breaking changes early.
*Best for *
- Systems with multiple consumers and independent teams.
- Ensuring backward compatibility.
Tool
- Spring cloud contract
- Pact
2. Producer-Driven Contracts **
The producer defines the contract β the API specification β and shares it with consumers.
**How it works:
Producer publishes the OpenAPI/Swagger schema or a written contract.
Consumers build against this contract.
Best for
- Where contracts are mostly controlled by producer service.
Tools
OpenAPI/Swagger
3. Bidirectional Contracts
A combination of consumer and producer contracts β both sides define expectations, and they are reconciled in a central place.
Best for:
- Where contracts evolve from both sides.
Tools
PactFlow
Conclusion:
In a microservices architecture, communication contracts between services are critical β yet easy to break. By automating contract validation, generating stubs, and catching breaking changes early in the pipeline, contract testing boosts confidence, improves collaboration across teams, and helps deliver more reliable systems.
In the market we have different tools to use contracts based on your use case.
Top comments (0)