Backward Compatibility & Contract Testing
IntermediateConsumer-Driven Contract Testing (Pact) verifies that provider APIs remain compatible with consumer expectations, catching breaking changes before they reach production.
Overview
In a microservice architecture, independent deployments create a version compatibility problem: when Service A (consumer) depends on Service B (provider), changes to B's API may break A without either team knowing until production. Consumer-Driven Contract Testing (CDCT) addresses this by having the consumer write expectations (a "contract") about the provider's API, and the provider verifies it can satisfy those contracts in its own CI pipeline. Pact is the most popular CDCT framework. The alternative — schema evolution (Avro, Protobuf, JSON Schema Registry) — enforces compatibility rules at the message schema level for event-driven systems. Both approaches catch breaking changes before they reach production.
Consumer-Driven Contract Testing with Pact
The consumer defines a Pact contract — the minimum response shape it needs from the provider. The Pact broker stores contracts. The provider downloads contracts from the broker and verifies it can produce the expected responses. If the provider changes its response in a breaking way, the verification fails in CI before deployment.
// Consumer side — define contract in a consumer test
@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "inventory-service")
public class InventoryClientContractTest {
@Pact(consumer = "order-service")
public RequestResponsePact stockLookupPact(PactDslWithProvider builder) {
return builder
.given("product 101 exists with stock 50")
.uponReceiving("a request for product stock")
.path("/stock/101")
.method("GET")
.willRespondWith()
.status(200)
.body(new PactDslJsonBody()
.integerType("productId", 101) // type-only matching (flexible)
.integerType("quantity", 50)
.stringType("unit", "units"))
.toPact();
}
@Test
@PactTestFor(pactMethod = "stockLookupPact")
public void testStockLookup(MockServer mockServer) {
InventoryClient client = new InventoryClient(mockServer.getUrl());
StockResponse response = client.getStock(101L);
assertThat(response.getQuantity()).isGreaterThanOrEqualTo(0);
// Pact generates a contract JSON file — publish to Pact Broker
}
}Provider verification in CI
The provider downloads contracts from the Pact Broker and runs them against a real (or started) provider. If the provider changes its response structure in a way that breaks the contract, the verification test fails. This catches breaking changes in the provider's CI pipeline before deployment — not in production.
// Provider side — verify contracts from Pact Broker
@Provider("inventory-service")
@PactBroker(
url = "https://pact-broker.example.com",
authentication = @PactBrokerAuth(token = "${PACT_BROKER_TOKEN}")
)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class InventoryProviderContractTest {
@LocalServerPort
private int port;
@BeforeEach
void setUp(PactVerificationContext context) {
context.setTarget(new HttpTestTarget("localhost", port));
}
// Set up test state matching "given()" in consumer contract
@State("product 101 exists with stock 50")
public void setupProduct101() {
// Insert test data into database or mock the repository
inventoryRepository.save(new Product(101L, "Widget", 50, "units"));
}
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
context.verifyInteraction();
}
}
// Publish results to Pact Broker (in CI)
// pact.verifier.publishResults=true
// pact.provider.version=${GIT_COMMIT}
// pact.provider.branch=${GIT_BRANCH}Schema evolution rules for Avro and JSON Schema
For event-driven systems using Kafka with a Schema Registry, compatibility rules enforce that schema changes remain compatible. BACKWARD compatibility (new schema can read old messages) is the default for consumer evolution. FORWARD compatibility (old schema can read new messages) is needed when producers change first. FULL compatibility enforces both.
# Confluent Schema Registry compatibility modes
# BACKWARD (default): new schema can read messages from previous schema
# → consumers can be upgraded before producers
# → safe to: add optional fields, remove fields
# → NOT safe to: remove required fields, change field types
# FORWARD: old schema can read messages from new schema
# → producers can be upgraded before consumers
# → safe to: add fields with defaults
# FULL: both BACKWARD + FORWARD
# → safe additions only: new optional fields with defaults
# Set compatibility for a subject
curl -X PUT http://schema-registry:8081/config/orders-value \
-H "Content-Type: application/json" \
-d '{"compatibility": "FULL"}'
# Avro schema evolution example (BACKWARD compatible change)
# V1: {"type":"record","name":"Order","fields":[
# {"name":"id","type":"int"},
# {"name":"total","type":"double"}
# ]}
# V2: add optional field with default (BACKWARD compatible)
# {"name":"customerId","type":["null","string"],"default":null}
# NOT compatible (would break BACKWARD): removing "total" fieldKey Points to Remember
- 1Consumer-Driven Contract Testing catches breaking API changes in provider CI before they reach production
- 2Pact contracts define minimum required response shape — type-matching (not value-matching) makes contracts resilient
- 3Provider state ("given()" in Pact) sets up test data to match each consumer scenario — must be deterministic
- 4Publish verification results to Pact Broker with git commit/branch — enables "can-i-deploy" checks in deployment pipelines
- 5Avro/JSON Schema BACKWARD compatibility: add optional fields; NOT backward compatible: remove/rename required fields
- 6FULL compatibility (add optional fields with defaults) is the safest evolution strategy for bidirectional compatibility
Interview Questions
Sign in to ask AriaWhat problem does Consumer-Driven Contract Testing solve that integration tests cannot?
Who writes Pact contracts — the consumer or the provider?
What is BACKWARD schema compatibility in Avro and what changes are safe to make?
How does the "can-i-deploy" check in Pact Broker prevent breaking deployments?
How would you handle a breaking change to an API that has consumer contracts written against it?
Ask Aria about Backward Compatibility & Contract Testing
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.