Contract testing solves the fundamental problem of microservice integration: how do you know that changing Service A won’t break Service B, C, and D? Integration tests are slow and brittle. Contract tests are fast, isolated, and catch breaking changes before they reach production.
Why Contract Testing Exists
Without contracts:
Service A changes response format
→ Deploys successfully (its own tests pass)
→ Service B starts failing (unexpected field type)
→ Service C crashes (missing required field)
→ Incident at 2 AM
With contracts:
Service A changes response format
→ Contract test fails in CI
→ PR blocked
→ Developer fixes compatibility
→ Safe deploy
Consumer-Driven vs Provider-Driven
| Approach | Who Defines the Contract | Best For |
|---|
| Consumer-driven | Consumers define what they need | Internal microservices |
| Provider-driven | Provider publishes its schema | Public APIs, third-party integrations |
| Bi-directional | Both sides verify against a shared spec | OpenAPI-first development |
Consumer-Driven Contract Testing (Pact)
1. Consumer writes a test:
"When I call GET /users/123, I expect { id: 123, name: string, email: string }"
2. Pact generates a contract file (JSON):
{ interactions: [{ request: ..., response: ... }] }
3. Contract published to Pact Broker
4. Provider verification:
Provider replays the contract interactions against its actual API
If any interaction fails → Provider knows it would break the consumer
| Tool | Language Support | Contract Format | Broker | Best For |
|---|
| Pact | 12+ languages | Pact JSON | Pact Broker / Pactflow | Multi-language ecosystems |
| Spring Cloud Contract | Java/Kotlin | Groovy DSL / YAML | N/A (CI artifact) | Spring ecosystem |
| Specmatic | JVM | OpenAPI spec | N/A | OpenAPI-first teams |
| Dredd | Any (HTTP) | API Blueprint / OpenAPI | N/A | API documentation testing |
Schema Evolution Rules
| Change Type | Safe? | Contract Impact |
|---|
| Add optional field to response | ✅ Yes | Existing consumers ignore it |
| Add required field to response | ⚠️ Depends | Safe if consumers use tolerant readers |
| Remove field from response | ❌ No | Breaks consumers that depend on it |
| Change field type | ❌ No | Breaks deserialization |
| Add optional field to request | ✅ Yes | Existing requests still valid |
| Add required field to request | ❌ No | Breaks existing consumers |
| Rename field | ❌ No | Breaks serialization on both sides |
| Add new endpoint | ✅ Yes | No existing consumer affected |
| Remove endpoint | ❌ No | Breaks any consumer calling it |
Pact Workflow in Detail
Consumer Side
Test: "User service client"
Given: User 123 exists
When: GET /users/123
Then: Response contains:
- status: 200
- body: { id: integer, name: string, email: string }
- headers: { Content-Type: "application/json" }
→ Generates: user-service-consumer-user-service-provider.json
→ Published to Pact Broker
Provider Side
Verification:
Load contract from Pact Broker
For each interaction:
Set up provider state ("User 123 exists")
Replay request (GET /users/123)
Compare actual response against contract expectations
Report: PASS or FAIL with diff
Pact Broker
| Feature | Purpose |
|---|
| Contract storage | Central repository for all contracts |
| Verification status | Track which provider versions verified which contracts |
| Can-I-Deploy | Binary check: “Is it safe to deploy this version?” |
| Webhooks | Trigger provider verification when new contracts are published |
| Network diagram | Visualize service dependencies automatically |
Integration with CI/CD
Consumer PR:
→ Unit tests
→ Contract tests (generate Pact file)
→ Publish contract to Pact Broker
→ Trigger provider verification (webhook)
→ can-i-deploy check
→ Merge
Provider PR:
→ Unit tests
→ Verify all consumer contracts from Pact Broker
→ can-i-deploy check
→ Merge
→ Deploy
Testing Async / Event-Driven Contracts
| Pattern | How to Test |
|---|
| Message queues | Pact supports message-based contracts |
| Event schemas | Use Avro/Protobuf schema registry as contract source |
| Webhooks | Consumer defines expected webhook payload format |
| GraphQL | Contract on query/mutation shapes and response structures |
Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|
| Testing every field | Contracts become brittle, break on any change | Test only fields the consumer actually uses |
| No Pact Broker | Contracts shared via git = stale contracts | Use Pact Broker for real-time contract management |
| Provider state ignored | Tests fail because test data doesn’t match | Implement provider state handlers for each scenario |
| Testing internal microservices like public APIs | Over-specification, slow evolution | Consumer-driven contracts allow flexibility |
No can-i-deploy gate | Contracts exist but aren’t enforced | Add can-i-deploy as a CI gate before merge |
Checklist
:::note[Source]
This guide is derived from operational intelligence at Garnet Grid Consulting. For microservices architecture consulting, visit garnetgrid.com.
:::
Jakub Dimitri Rezayev
Founder & Chief Architect • Garnet Grid Consulting
Jakub holds an M.S. in Customer Intelligence & Analytics and a B.S. in Finance & Computer Science from Pace University. With deep expertise spanning D365 F&O, Azure, Power BI, and AI/ML systems, he architects enterprise solutions that bridge legacy systems and modern technology — and has led multi-million dollar ERP implementations for Fortune 500 supply chains.
View Full Profile →