Internal Contracts
Contract testing, consumer-driven contracts, Pact, and contract-first design to ensure compatibility between services.
What are internal contracts?
In a distributed system, each service exposes interfaces that other services consume. An internal contract is the agreement — explicit or implicit — about the shape, types, and semantics of those interfaces. When a service changes its contract without coordinating with its consumers, things break.
Internal contracts formalize these dependencies so they can be verified automatically, before a change reaches production.
Contract Testing
Contract testing verifies that two services (provider and consumer) agree on the shape of their communication. Unlike end-to-end integration tests, contract tests run independently for each service.
Why aren’t integration tests enough?
- Integration tests require all services to be running simultaneously
- They’re slow, brittle, and hard to maintain
- They don’t scale when you have dozens of services
- A failure doesn’t always clearly indicate which service broke the contract
How contract testing works
- The consumer defines what it expects from the provider (the contract)
- The contract is verified against the provider independently
- If the provider satisfies the contract, both services are compatible
- If it doesn’t, the test fails before reaching production
Consumer-Driven Contracts (CDC)
In the consumer-driven approach, it’s the consumers who define the contracts. This flips the traditional dynamic where the provider defines its API and consumers adapt to it.
CDC workflow
- The consumer writes a contract: Defines the interactions it needs — what requests it makes and what responses it expects
- The contract is shared: Published to a broker or shared repository
- The provider verifies: Runs the contracts from all its consumers against its implementation
- Fast feedback: If a change in the provider breaks any contract, the pipeline fails
Advantages of the CDC approach
- Providers know exactly what their consumers use
- Breaking changes can be made with confidence — you know who’s affected
- Consumers only define what they actually need, not the whole API
- Reduces coupling — the provider can change what nobody uses
Pact
Pact is the most popular tool for consumer-driven contract testing. It supports multiple languages (Java, JavaScript, Python, Go, .NET, Ruby) and provides an end-to-end workflow.
Key Pact concepts
- Pact file: A JSON file describing the expected interactions between consumer and provider
- Pact Broker: A centralized service that stores and manages pact files
- Can-I-Deploy: A tool that checks whether it’s safe to deploy a specific version
Typical Pact workflow
Consumidor Pact Broker Productor
| | |
|-- genera pact file --------->| |
| | |
| |<-- descarga pact files ------|
| | |
| |<-- publica resultados -------|
| | |
|-- can-i-deploy? ------------>| |
|<-- sí/no -------------------| |
Example Pact interaction
On the consumer side, the expected interaction is defined:
Dado: que existe un producto con ID 123
Cuando: hago GET /api/products/123
Entonces: recibo status 200 con body { id: 123, name: "string", price: "number" }
On the provider side, Pact replays the interaction against the real implementation and verifies that the response matches.
Contract-First Design
In the contract-first approach, the contract is defined before writing any code. This is especially useful when multiple teams are working in parallel.
Contract-first workflow
- Define the contract: Using OpenAPI, AsyncAPI, Protobuf, or another specification format
- Review and agree: The provider and consumer teams review and approve the contract
- Generate code: Use code generation tools to create stubs, clients, and validators
- Implement: Each team implements its part against the agreed contract
- Verify: Automated contract tests validate that both implementations satisfy the contract
Tools for contract-first
| Tool | Type | Use |
|---|---|---|
| OpenAPI / Swagger | REST APIs | Defining endpoints, schemas, and examples |
| AsyncAPI | Event-driven | Defining channels, messages, and schemas |
| Protobuf | gRPC | Defining services and typed messages |
| JSON Schema | General | Validating data structures |
Best practices
- Start with contract-first for new APIs — it’s easier than retrofitting contracts later
- Use a Pact Broker (or equivalent) to centralize and version contracts
- Integrate contract tests into CI/CD — make them fail before reaching staging
- Don’t test the whole API in the consumer’s contract — only what you actually use
- Version contracts alongside the code — a contract is as important as the code that implements it
- Review contracts in code review — a contract change deserves the same attention as a database schema change