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

  1. The consumer defines what it expects from the provider (the contract)
  2. The contract is verified against the provider independently
  3. If the provider satisfies the contract, both services are compatible
  4. 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

  1. The consumer writes a contract: Defines the interactions it needs — what requests it makes and what responses it expects
  2. The contract is shared: Published to a broker or shared repository
  3. The provider verifies: Runs the contracts from all its consumers against its implementation
  4. 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

  1. Define the contract: Using OpenAPI, AsyncAPI, Protobuf, or another specification format
  2. Review and agree: The provider and consumer teams review and approve the contract
  3. Generate code: Use code generation tools to create stubs, clients, and validators
  4. Implement: Each team implements its part against the agreed contract
  5. Verify: Automated contract tests validate that both implementations satisfy the contract

Tools for contract-first

ToolTypeUse
OpenAPI / SwaggerREST APIsDefining endpoints, schemas, and examples
AsyncAPIEvent-drivenDefining channels, messages, and schemas
ProtobufgRPCDefining services and typed messages
JSON SchemaGeneralValidating 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