Dec 17, 2024·7 min

Integration Contracts and Contract Tests—No Surprises

Integration contracts and contract tests lock the API format, let you change integrations safely and catch errors before release. Steps, example and checklist.

Integration Contracts and Contract Tests—No Surprises

Problem: integrations break at the worst time

Integrations usually break not because of big refactors but because of small things. A field was renamed. A type changed from number to string. A new required parameter was added. Status 200 was changed to 204. For the team making the change it looks like an improvement. For the downstream system it’s a sudden failure.

From the outside these failures always look the same: yesterday everything worked, today users see errors and logs turn into chaos.

Usually this appears as:

  • 500 or 502 “out of nowhere”, even though “we didn’t change anything”
  • empty fields in responses that break business logic
  • unexpected statuses (for example, expected 404 but got 200 with an empty body)
  • timeouts caused by new heavy requests or extra calls
  • “silent” errors: data was written but incorrectly (date in a different format, units swapped)

Manual checks and chat discussions rarely save the day. People usually test the “happy path”, not all variations. Agreements live in messages: someone is on vacation, someone didn’t see it, someone understood it differently. As a result the integration becomes fragile and relies on people’s memory.

To lock down the exchange format means to agree and record exactly what you send and receive: which fields exist, which are required, which values are allowed, which statuses and errors may occur, and what the response time limits are. It’s important not only to record this but to automatically verify it during changes. That’s what integration contracts and contract tests do: they catch mismatches before release, when fixing is still cheap and fast.

Imagine an integration between an accounting system and a reporting service. If one side suddenly starts sending status: "ok" instead of status: "success", a manual test may miss it and the report will break only at the end of the month. A contract check will catch the discrepancy immediately, during the build.

What an integration contract is and what it includes

An integration contract is an explicit agreement between a provider and a consumer about how they exchange data and how the interface behaves. It’s not just “which fields are in JSON”, but also what is considered a valid request, what responses are possible and what errors should be predictable.

A good contract describes testable rules, not intentions. To act like insurance it typically includes:

  • data format: fields, types, constraints (e.g. string length, numeric ranges), date formats
  • requiredness: what must always be present, what can be missing, what depends on conditions
  • statuses and codes: which statuses are allowed and what they mean
  • errors: error structure, codes, common causes, what can be retried and what must be fixed
  • compatibility: which changes are safe (e.g. adding an optional field) and which break clients

A contract is often confused with documentation. Documentation is for humans: examples, scenarios, tips. A contract is for machines: it must be precise enough to be automatically checked by a test. If documentation says “field may be empty”, the contract must state exactly what is allowed: an empty string, null or the absence of a field, and how to interpret it.

Contracts are especially important when there are many teams and consumers: one service feeds multiple apps, different vendors build clients, and interfaces outlive individual people. In such cases the contract becomes a single source of truth and reduces incident investigation time.

Types of contracts: from APIs to events

A contract can be described in different ways, but the idea is the same: record what data in what shape is exchanged between systems. Two common approaches are consumer-driven and provider-driven.

A consumer-driven contract is convenient when a service has several clients and each uses its own slice of the API. The consumer states expectations: which fields are needed, which values are allowed, which error codes are acceptable. This reduces the risk that the provider will accidentally break your specific scenario while “overall it still works”.

A provider-driven contract is simpler as a first step when one service reliably serves many clients and the team wants to quickly set order. The provider describes the specification and guarantees, and consumers verify compatibility on their side.

Synchronous APIs and asynchronous events

Contracts are not only about HTTP APIs.

For synchronous APIs a contract fixes requests, responses and errors: required fields, types, date formats, string length limits.

For asynchronous events and messages the structure of the event, schema version, idempotency keys and rules for handling unknown fields are more important. For example, an asset management service publishes a "DeviceRegistered" event and the warehouse service consumes it. If serialNumber suddenly becomes a number instead of a string, the consumer may not fail immediately but crash later when the queue grows.

Choice of approach often depends on people and change speed:

  • 1–2 consumers and frequent changes: consumer-driven is usually preferable
  • many consumers and rare changes: provider-driven is easier to start with
  • different teams and high misunderstanding risk: a mixed approach works (provider sets the base, key consumers refine)
  • asynchronous integrations: agree on event versioning and compatibility rules from the start

Contract tests: what they check and what they don’t

Contract tests are needed to verify in advance that the producer and consumer still understand each other. If the contract changes, tests fail before deployment and the team sees the problem in CI, not at a late-night incident.

The main difference from integration and e2e tests is scope. Integration tests bring up real dependencies and catch failures at system boundaries. E2e tests run a long user scenario across multiple services. Contract testing only checks the agreement about format and exchange rules. Because of its focus it’s usually faster and more accurate in its area.

These tests catch small things that immediately break clients:

  • a required field disappeared or its type changed (number became string)
  • a new required field was added without a default
  • status codes and error codes changed (e.g. 200 instead of 201, or 404 instead of 400)
  • headers changed or new header requirements appeared (e.g. mandatory correlation-id)
  • nested object or array structure was altered

Simple example: a mobile app expects price as a number, but the backend started returning "price": "12000". A contract test will fail immediately, even if a manual check of one screen missed it.

It’s important to understand the boundaries. Contract tests do not answer whether the system works correctly in meaning. They answer whether the format is compatible.

They usually do not check:

  • business logic correctness and calculations (discounts, limits, access rules)
  • data state and migrations (whether required records exist in the DB)
  • performance and latency under load
  • long chains of scenarios across multiple services

If you keep this focus, contract tests become a reliable safety net: they quickly find incompatible changes and save time investigating integration issues.

How to agree on a format without extra bureaucracy

Vendor-neutral solution selection
We will choose a solution considering your vendors and requirements, keeping supply transparency.
Start a conversation

Agreements about data exchange usually break on details: a field becomes required, an error is returned in a different shape, a value no longer fits the type. For a contract to work it must be simple, clear and maintainable.

Start with a short description of participants: who is the provider (sends data), who are consumers (read it), and which scenarios are truly critical. For example: the support service creates a ticket and the asset management system must accept it within 2 seconds and return an ID. That’s more important than rare administrative methods that almost nobody uses.

Then record specifics, not a half-page text:

  • the minimal set of methods or events and 2–3 critical scenarios
  • example requests and responses (including required and optional fields)
  • error format: codes, messages, where to find details, correlation id (if used)
  • contract version and how a client knows which version it uses
  • what counts as a breaking change (and what can change safely)

Agree on change rules once and record them. A practical boundary: adding a new optional field usually doesn’t break clients, while renaming a field or changing its type or meaning almost always does. When in doubt, treat the change as breaking until the consumer confirms otherwise.

Don’t forget owners. Without them the contract quickly becomes “a file nobody opens”. At minimum agree on three roles: the provider updates the contract when changing, the consumer confirms their scenarios, and a single responsible person (team lead or API owner) resolves disputes.

Step-by-step: how to introduce contract tests into a project

To make checks actually catch problems, start small. The initial goal is to protect the most important integration points, not to describe the whole world.

Minimal start in 1–2 weeks

Choose a few scenarios where failures hurt the most: authentication, creating a ticket, price calculation, status delivery. Usually 3–5 operations or events that are used most and change most often are enough.

Collect real examples of requests and responses: logs, test dumps, examples from docs. Then add edge cases: empty strings, very long values, unknown enum values, missing optional fields. These data sets often reveal the “invisible” issues.

As a result the team gets a common standard: what counts as correct exchange and what should fail tests.

Tests on consumer and provider

Split checks between the two sides. The consumer verifies it forms requests correctly and can parse responses. The provider verifies it returns the response in the required structure and didn't remove an important field.

Integrate checks into CI so a build fails if the contract is violated.

Then define a clear change process: what can be extended safely (e.g. add optional fields) and what requires a new version (remove, rename, change type). Also set a timeline for when the old format will be turned off so you don’t keep “eternal compatibility”.

Where to store the contract and how to manage changes

The most reliable place is next to the code of the service that publishes it. Then format changes don’t happen “accidentally”: they go through normal development, commit history and release process.

A contract must be tied to a service version. You don’t need complexity: agree that the contract lives in the repo and its version matches the release version or is tagged. The main point is that a single version number answers: “which contract is in production now?” and “which contract to expect in the next release?”.

It’s useful to separate changes by intent. If a change is backwards compatible (added an optional field that can be ignored), that’s one track. If it breaks consumers (renamed field, changed type), that’s a different track and usually a new version.

To avoid drowning in approvals, use a simple contract review rule:

  • the author describes what changes and why (2–3 sentences in the PR)
  • the API owner or team lead confirms the change is justified
  • a representative of a key consumer (or integration team) checks their scenario won’t break
  • the release manager ensures there is a rollout and rollback plan

Notify consumers in a predictable way. Minimal set: a note in release notes, automatic CI checks and a predefined migration window. For example: “breaking changes are released only with a new API version and we give 2–4 weeks for migration”, and the old version is kept for one more release cycle.

Common mistakes and pitfalls with the contract approach

PCs for development and testing
We will select domestic GSE L200 Series PCs for development and QA teams.
Select a PC

The contract approach usually fails not because of tools but because of small decisions when changing things.

The most common trap is renaming or removing a field without a compatibility period. The provider thinks: “Field is obsolete, remove it.” The consumer thinks: “It will be there forever.” The integration breaks at night and everyone looks for who’s to blame rather than why it happened.

Even more dangerous are hidden type changes. Today it’s a number, tomorrow a string (e.g. "00123" to preserve leading zeros). A parser might coerce silently and the error surfaces later in business reports. The contract should lock not only field presence but also type, format and constraints.

A separate issue is new required fields without defaults and migration. If you add a required field without a default or a migration plan, you create an “unupdatable” integration.

People often confuse contract and business logic. Contract tests check format, statuses, requiredness and validation rules at the boundary. They shouldn’t prove that a discount was calculated correctly.

Some rules that help keep things under control:

  • rename or removal through compatibility period: add the new field, support both, then remove the old one
  • type or format changes (date, money, identifiers) require an explicit contract update and tests
  • new required fields must come with a default or a transition period
  • separate contract tests from business-rule tests to avoid brittle checks
  • add negative cases: 400 for invalid body, 401 without auth, 404 for missing resource, 409 for version or state conflicts

Simple example: the orders service started requiring a new sourceSystem field. Tests had only happy scenarios, so the release passed and old clients began receiving 400. Negative checks in the contract would have caught this before deployment.

A short checklist before releasing API changes

Before release run the same short checklist. It takes 10–15 minutes but often saves hours of post-release investigation.

Check:

  • required fields: none disappeared, types didn’t change, date formats, enum values and validation rules
  • statuses and errors: response codes, error body structure and field meanings (e.g. code/message/details)
  • compatibility for extensions: new fields should be optional with safe defaults
  • expectations and limits: timeouts, request/response size limits, pagination and sorting rules
  • CI run: checks must pass for both provider and consumer

A practical trick: ask a developer who did not make the change to open the contract and answer: “Can the old client continue to work?” If the answer is not obvious, clarify the contract or add a test while fixes are still cheap.

A real-life example: catching a break before production

All-in-ones for workstations
We will choose touchscreen GSE M200 Series all-in-one PCs for workstations and service areas.
Select AIO

Imagine a typical chain: an external accounting system sends documents (invoices, waybills) to an internal service that routes data to warehouse and finance. Everything works until one side “slightly improves” the format.

The provider added a deliveryDate field and accidentally changed the type of the old amount field: it was a number, became a string (for example, due to formatting). On the test environment this was barely noticed: the UI “shows”, reports “compile”, and rare errors were blamed on test data.

Contract testing caught it before deployment. The provider’s pipeline includes a check that the current response or message matches the recorded contract. The test failed immediately because amount no longer matched the expected type:

{
  "docId": "INV-10452",
  "amount": "12500.50",
  "currency": "KZT",
  "deliveryDate": "2026-01-11"
}

The team quickly resolved it: adding a new optional field is fine, but changing the type of an existing field requires a migration step. The fix was simple: return amount as a number again and keep deliveryDate optional (with clear behavior when it’s absent).

To prevent repeats they agreed on rules:

  • any change to type, name or meaning of a field is a breaking change and requires a new version
  • new fields are added only as optional until consumers migrate
  • versions are coordinated in advance: who, when and how switches
  • contract tests run on every schema change, before merging into the main branch

Next steps: how to start in your organization

The fastest way to get value from the contract approach is not to try to cover everything at once. Pick 1–2 most painful integrations: where nightly incidents, manual checks and urgent rollbacks happen most. It’s easier to agree on the format, write the first checks and show the benefit there.

Then assign contract owners. You need a responsible person on the provider side and one on the consumer side: who approves changes, who ensures backward compatibility and who monitors test results.

Minimal plan for 2–4 weeks

  • record a contract for one integration (API or event) and agree on change rules
  • integrate checks into CI so they run on every merge and before release
  • set a cadence: checks on each release branch and more frequently for critical integrations
  • prepare a separate test environment so builds and tests don’t affect production
  • adopt a simple rule: a failing contract test blocks the release

A test environment is often the bottleneck. If many services are involved you need resources for builds, test databases and data isolation. It’s important to decide in advance who provides resources and who ensures environment stability.

If you lack integration experience or CI/test environment setup in-house, it can be easier to involve a system integrator. For example, GSE.kz (gse.kz) as a vendor and system integrator can help design infrastructure for test contours and select server capacity so checks are fast and repeatable.

FAQ

What is an integration contract in simple terms?

An integration contract is a testable description of which requests, responses and errors are considered correct between two systems. It records data structure, required fields, types, allowed values, statuses and error format so it can be automatically verified by tests.

How does a contract differ from regular API documentation?

Documentation helps a person understand how to use an API: examples, scenarios and explanations. A contract is for machines: it must be unambiguous and testable, without vague phrases like “may be empty”, so CI can catch incompatibilities before release.

Which changes most often break clients?

Clients are most often broken by renaming fields, changing a value type, removing a required field, or adding a new required parameter without a default. Another common source of problems is changing response codes or error structure when the client expects something else.

What exactly do contract tests check?

Contract tests verify format and boundary behavior: JSON structure, types, required fields, statuses, errors and header requirements. They don’t prove business logic correctness, but they quickly show that the client and server no longer “speak” the same language.

How to start implementing contract tests if time is limited?

Start with 3–5 most critical operations or events where failures hurt the most: authentication, creating an entity, calculation, getting a status. Grab real examples from logs and add edge cases so the contract catches issues beyond the happy path.

When to choose consumer-driven vs provider-driven contracts?

If a service has multiple clients and each uses its own slice of the API, consumer-driven contracts are more convenient: the client states its expectations and protects its scenario. If you need to quickly set a single set of guarantees for all consumers, start provider-driven and then refine important scenarios with key clients.

How does the contract approach work for messages and events, not just HTTP APIs?

For events, the important things are message schema, versioning, compatibility rules and how unknown fields are handled. A good practice is to agree in advance whether new fields can be ignored, how to distinguish versions, and which changes are breaking so consumers don’t fail due to unexpected types or formats.

How to decide whether a change is a breaking change?

By default, treat removal and renaming of fields, type changes, changes in meaning, date or money format changes, and new required fields without a transition period as breaking. Safest approach is to add only optional fields and keep old behavior until consumers migrate.

Where to store a contract and how to manage versions?

Keep the contract next to the provider’s code and version it with releases so changes go through normal code review and history. It’s important that a version lets you answer: “what contract is in production now?” and “what contract to expect in the next release?”.

What to do if a contract test fails in CI before release?

Reproduce the issue on a minimal example and compare the actual response with the contract: field, type, status, error format, headers. Then decide who should change — provider, consumer, or the contract — and add a test for the case so it won’t recur.

Integration Contracts and Contract Tests—No Surprises | GSE