Consumer-Driven Contract Testing at the Gateway

Schema validation at the gateway is a runtime backstop: it rejects a contract violation at the moment a request or response crosses the boundary. That is valuable, but it is late — the breaking change has already been written, merged, and deployed, and the gateway’s rejection is now a production incident rather than a caught mistake. Consumer-driven contract (CDC) testing moves the same check left into CI, where a provider build fails before it deploys if it would break any known consumer. This page shows how to wire Pact-style CDC verification together with the gateway schema validation described in backward-compatibility contracts so that a breaking change is caught in the pipeline and, if it somehow slips through, still rejected on the wire.

Prerequisite concepts

This builds directly on backward-compatibility contracts — you should already be treating additive versus breaking changes as a taxonomy and enforcing request and response schema at the gateway. It also assumes the framing from Advanced Routing & API Versioning: the gateway is the boundary where the externally promised contract is pinned and versioned, decoupled from any single upstream’s release cadence. If a change is genuinely breaking, CDC testing tells you who it breaks; an API versioning strategy tells you how to ship it anyway under a new version.

The two-layer model: CI contracts plus runtime enforcement

CDC and gateway validation are not competitors — they are two layers of the same defence, one at build time and one at request time. The consumer records its expectations as a contract; a broker stores it; the provider’s CI verifies against it; and the union of consumer expectations becomes the published contract the gateway enforces at runtime. The diagram traces that flow.

Consumer-driven contract flow: CI verification plus gateway runtime enforcement Consumer unit tests generate a Pact contract that is published to a contract broker. The provider CI pulls contracts and runs verification, gated by a can-i-deploy check before release. The same union of expectations is compiled into an OpenAPI or JSON schema that the gateway loads for runtime request and response validation. BUILD TIME (CI) RUN TIME (gateway) Consumer tests record expectations Contract broker stores Pact files Provider verify replay each interaction can-i-deploy gate before release Published contract OpenAPI / JSON Schema Gateway validation reject live violations union of expectations runtime backstop for gaps CDC missed

The key insight: CDC verification proves the provider still satisfies every consumer’s recorded subset, while the gateway enforces one published superset contract. A change that removes a field passes gateway validation if the field was optional, but fails CDC verification the instant a consumer recorded an expectation on it — which is why you want both.

Step 1 — Record the consumer contract

The consumer writes a test that stands up a mock provider and asserts the shape it depends on. Pact records this as an interaction. Crucially, the consumer only pins the fields it actually uses — a tolerant reader records no expectation on fields it ignores, which keeps the provider free to evolve them.

// Pact consumer test (JS) — records only the fields this consumer reads
const { PactV3, MatchersV3 } = require('@pact-foundation/pact');
const { like, uuid, term } = MatchersV3;

const provider = new PactV3({ consumer: 'billing-ui', provider: 'orders-api' });

provider
  .given('order 7f3a exists')
  .uponReceiving('a request for an order')
  .withRequest({ method: 'GET', path: '/v2/orders/7f3a' })
  .willRespondWith({
    status: 200,
    headers: { 'Content-Type': 'application/json' },
    body: {
      id: uuid('7f3a2b10-0000-4000-8000-000000000000'),
      status: term({ matcher: 'pending|paid|shipped|cancelled', generate: 'paid' }),
      total: like(42.50),
      // no expectation on `note` — this consumer does not read it
    },
  });

Publishing this on every consumer build pushes the contract to the broker, tagged with the consumer version and branch.

Step 2 — Verify the provider against every contract in CI

The provider build pulls all consumer contracts from the broker and replays each interaction against the real provider (or a built artifact of it). If the provider no longer returns total, or narrows the status matcher, verification fails and the build is blocked.

# Provider CI — verify against broker, block merge on failure
pact-provider-verifier \
  --provider "orders-api" \
  --provider-base-url "http://localhost:8080" \
  --broker-url "$PACT_BROKER_URL" \
  --consumer-version-selectors '{"mainBranch": true}' \
  --consumer-version-selectors '{"deployed": true}' \
  --publish-verification-results \
  --provider-app-version "$GIT_SHA"

Before releasing, a can-i-deploy gate asks the broker whether this provider version is compatible with the consumer versions currently in the target environment — this is the check that prevents deploying a provider that would break something already running in production.

# Release gate — only proceed if compatible with what's deployed
pact-broker can-i-deploy \
  --pacticipant "orders-api" \
  --version "$GIT_SHA" \
  --to-environment production

Step 3 — Compile the published contract and load it into the gateway

The union of consumer expectations is the minimum the provider must guarantee. Maintain the published OpenAPI/JSON Schema from that same source of truth and load it into the gateway so the runtime check and the CI check cannot drift. In Kong 3.x the openapi plugin enforces it; validate the spec itself in the same pipeline that runs verification.

# Kong 3.x — the gateway loads the same contract CDC verified against
plugins:
  - name: openapi
    config:
      spec: "@/etc/kong/specs/orders-v2.yaml"   # generated from the published contract
      validate_request_body: true
      validate_response_body: true               # canary tier
      notify_only_response_body: true            # log drift, don't 500 consumers

For an Envoy 1.32+ data plane, reuse the schema-owning sidecar from the parent page so the validator that CI runs and the validator ext_proc calls are the same binary — the strongest possible guarantee that build-time and run-time enforcement agree.

# Envoy 1.32+ — ext_proc points at the same validator image used in CI
http_filters:
  - name: envoy.filters.http.ext_proc
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.http.ext_proc.v3.ExternalProcessor
      grpc_service:
        envoy_grpc:
          cluster_name: contract_validator   # same schema artifact as CI
      failure_mode_allow: false              # fail closed on write paths
      processing_mode:
        request_body_mode: "BUFFERED"
        response_body_mode: "BUFFERED"       # canary only

Decision matrix

Concern Consumer-driven contracts (CI) Gateway schema validation (runtime)
When it runs Provider build, before deploy Every request/response on live traffic
What it proves Provider still meets each consumer’s exact expectations Traffic conforms to the one published contract
Catches Removed/renamed field a specific consumer reads; narrowed matcher Malformed input; response drift from the published superset
Blind spot A consumer that never wrote a contract Which consumer a change would break, and why
Failure cost Red build — cheap, pre-deploy Rejected request or logged drift — production
Source of truth Broker (union of consumer expectations) Published OpenAPI/JSON Schema (generated from same union)

Use both. CDC gives precise, consumer-specific feedback in CI; the gateway is the enforcement backstop for the traffic CDC’s known-consumer set does not cover — third-party integrators, ad-hoc scripts, and consumers that never onboarded to the broker.

Gotchas and failure signals

Contracts only cover known consumers. CDC verifies against consumers that publish to the broker. A public API with anonymous integrators has consumers you cannot enumerate, so the gateway’s published-contract validation is not optional — it is the only enforcement those consumers get. Signal: a field removal passes all CDC checks but breaks external users; caught only by gateway response validation or consumer complaints.

Over-specified consumer contracts create false breaks. A consumer that pins exact values (total: 42.50) instead of a type matcher (like(42.50)) fails verification on unrelated data changes, training teams to ignore red builds. Enforce matcher-based expectations in review.

Spec drift between CI and gateway. If the OpenAPI the gateway loads is hand-maintained separately from the contract CDC verifies, they diverge and one of them is lying. Generate both from one artifact and validate the gateway spec in the same pipeline. Signal: gateway rejects traffic that passed all CDC verification, or vice versa.

can-i-deploy skipped under deadline pressure. Bypassing the gate to ship faster reintroduces exactly the breaking-deploy risk CDC exists to prevent. Make it a required, non-overridable pipeline step.

Response validation left fail-closed in production. Turning notify_only_response_body off (or failure_mode_allow: false on responses) in production means an upstream adding a field can 500 real consumers. Keep production response checks notify-only or sampled; reserve fail-closed for request bodies on write paths.

Validation

  • Every internal consumer publishes a Pact contract to the broker on each build, using matchers rather than exact values.
  • Provider CI verifies against mainBranch and deployed consumer selectors and blocks merge on failure.
  • A can-i-deploy --to-environment production gate runs before release and is non-overridable.
  • The gateway’s OpenAPI/JSON Schema is generated from the same published contract, not hand-maintained separately.
  • Request validation is inline for write paths; response validation is notify-only or sampled in production, strict in canary.
  • The Envoy ext_proc validator (or Kong openapi spec) is the same artifact CI verifies against.
  • Gateway validation-rejection rate and validator health are alerted on, covering consumers that never onboarded to the broker.

FAQ

How is consumer-driven contract testing different from gateway schema validation?

Consumer-driven contract testing runs in CI and verifies the provider against the exact expectations each consumer recorded, so it catches a break before anything deploys. Gateway schema validation runs at runtime and enforces a single published contract on live traffic. They are complementary: CDC tells you which specific consumer a change would break and why, while the gateway is the enforcement backstop that rejects violations the tests did not cover.

Can I generate a gateway JSON schema from Pact contracts?

Not directly one-to-one, because Pact interactions capture example-based expectations and matching rules rather than a complete schema. The practical pattern is to derive an OpenAPI or JSON Schema document from the union of consumer expectations and use that as the gateway’s validation spec, keeping both generated from the same source of truth so the CI check and the runtime check cannot drift apart.

Should the gateway validation spec be the provider contract or the consumer contract?

The gateway enforces the provider’s published contract, which must be a superset that satisfies every consumer contract. Consumer-driven tests prove the provider still meets each consumer’s subset of expectations. If a consumer needs a field the published contract does not guarantee, that gap surfaces in CDC verification, not at the gateway.

Where should contract verification sit in the CI pipeline?

Publish consumer contracts on every consumer build, then run provider verification against the broker on every provider build and block merge on failure. Add a can-i-deploy gate before release that checks the provider version is compatible with the consumer versions currently in the target environment. The gateway spec is validated in the same pipeline so the runtime contract and the tested contract ship together.


Parent: Backward-Compatibility Contracts