API Contract Compatibility
Why Contract Compatibility
Contract compatibility is the ability of an API to evolve without breaking existing integrations. In growing systems, APIs change more often than client code; compatibility allows you to release features iteratively, without arranging "big moves."
The key idea: the contract is primary, changes are carried out according to compatibility rules and are checked automatically.
Basic concepts
Contract - formal interface specification: resources/methods/events, data schemas, error codes, limits, SLAs, security requirements.
Provider - The owner of the API. Consumer - Client/Integration.
- Backward: The new supplier works with old consumers.
- Forward: The old supplier works with new consumers (usually achieved by "tolerant readers").
- Full: both backward and forward (the strongest option) are observed.
- Additivity - add optional elements without breaking existing ones.
Versioning Policy
Semantic Versioning (recommended):- MAJOR - breaking changes (only when a new API line is released: '/v2 ',' service. v2`).
- MINOR - additive changes (new optional fields/methods).
- PATCH - fixes without changing the contract.
- Rejection Policy: declaration of obsolete elements, support window (sunset), warnings in headers/metadata, withdrawal plan.
Safe vs dangerous changes
Secure (usually backward-compatible)
Add an optional field to JSON/Protobuf/Avro.
Add a new endpoint/method/event.
Extending enum with new values if consumers are tolerant of unknown values.
Raising limits (for example, 'maxItems') without tightening the minimum.
Adding nullable with correct defaults.
Edit description/example text.
Dangerous (breaks compatibility)
Rename/delete fields, change their type or mandatory.
Status code/error semantics change (for example, was' 200 ', became' 204'or' 404 ').
Changing the format of identifiers (UUID → int).
Tightening of validation (stricter minimums/patterns) without version.
Changing the order and structure in gRPC streams/events.
Reuse tag numbers in Protobuf for new fields.
Interoperability by Interaction Style
REST/HTTP + JSON Schema
Additivity: we mark new fields as' optional '/' nullable '.
Tolerant Reader at the client: ignore unknown fields; not relying on order.
Versioning: major - on the way ('/v2 ') or in the media type (' application/vnd. example. v2+json`).
ETag/If-Match: for safe updates without racing.
Errors: single format ('type', 'code', 'title', 'detail', 'trace _ id'), do not change the value of'code' without a major.
Pagination: cursors are preferable to offset; add 'next _ cursor' fields, do not change the meaning of existing ones.
gRPC / Protobuf
Tag numbering is unchanged. Deleted tags cannot be reused.
The new fields are 'optional '/' repeated' with reasonable defaults on the server.
Do not change the order and mandatory messages in streaming-RPC.
Error statuses are stable ('INVALID _ ARGUMENT', 'FAILED _ PRECONDITION', etc.); new semantics → a new version of the method/service.
Event-driven (Kafka/NATS/Pulsar) + Avro/JSON Schema
Naming events: 'domain. action. v{major}`.
New fields are optional; isolate core and enrichment ('.enriched').
Schema registers: compatibility rules (BACKWARD/FORWARD/FULL) on theme/event.
The enum extension is valid for consumer-side tolerant reader.
Partition/order key change for aggregate = breaking changes.
GraphQL
Adding fields/types is safe; delete/rename - only through @ deprecated and the migration window.
Do not change types/non-nullable without major.
Control complexity/depth - limits are part of the contract.
Sustainable evolution patterns
Additive-first: Expand without breaking.
Capability negotiation: clients report that they support (headers/parameters/agreements), the server adjusts.
Contract boundaries: Fix MGC (minimum warranty contract) and separate extensions (reverse pyramid model).
Tolerance by default: clients ignore unnecessary and correctly handle unknown enum (fallback) values.
Dual-write/Dual-emit: for major changes, release 'v1' and 'v2' in parallel for a while.
Sunset headers/Events: Notify in advance when versions are removed.
Governance and Automation
API Linters:- OpenAPI/Spectral: naming, pagination, error codes, field formats.
- Buf/Protobuf: disallowing re-use of tags, packet notation.
- AsyncAPI/Schema Registry: CI-level schema compatibility.
- Contract Catalog (SSOT): Centralized schema/version register with diffuse history.
- API Guild: guild/committee that adopts rules, templates and review changes.
- Change Management: RFC/ADR, release notes, migration guides.
Compatibility testing
Schema-diff in CI: block breaking cakes (OpenAPI-diff, Buf breaking, SR compatibility).
Consumer-Driven Contracts (CDC): Pact/Similar - Supplier vs. Consumer-Specific Contracts.
Golden samples: reference queries/responses and events for regression.
E2E Canary: rolling out to the share of traffic/individual consummer groups.
Chaos/latency: Timeout/Retray check - A latency-SLO change is considered a contract change.
Migrations and deprecate
1. Declare deprecate: Mark the item, specify sunset term and alternative.
2. Maintain compatibility period: dual-write/dual-emit, bridges, adapters.
3. Collect telemetry: who else uses the old?
4. Communication: mailings, release notes, test stands.
5. Removal: after the window expires - removal with a fixed release.
Examples of changes
REST
It was:json
{ "id":"p1", "status":"authorized" }
Became (additive, safe):
json
{ "id":"p1", "status":"authorized", "risk_score": 0. 12 }
Clients that ignore unknown fields do not break.
Protobuf
proto message Payment {
string id = 1;
string status = 2; // don't change tag numbers optional double risk_score = 3; // additive
}
Event
`payment. authorized. v1 '(core) +' payment. enriched. v1 '(enrichment). Critical path consumers read the core and are not dependent on enrichment.
Antipatterns
Swagger-wash: there is formally a specification, but the behavior of the service is at odds with it.
Breaking by stealth: changed type/status/format without a new version and migration window.
Raw CDC events as a public contract: leaked DB schemes, impossibility of evolution.
Hard client: drops at unknown fields/values; absence of a tolerant reader.
Re-using protobuf tags: quiet data corruption.
Latency as "non-contract": p95 was unexpectedly lengthened - consumers break down in timeouts.
Compatibility checklist (before merge)
- Changes are additive (or major version prepared).
- Linters/diff checks passed, compatibility rules are green.
- Errors/codes/statuses did not change semantics.
- Enum extended without prohibiting old values; clients - tolerant.
- The boundaries of the MGC are unchanged.
- Updated samples/documentation/SDK.
- For major - dual-write/dual-emit plan, sunset-date, comm-plan.
- Tests CDC/Golden/E2E passed.
FAQ
How does backward differ from forward compatibility?
Backward - new servers do not break old clients. Forward - new clients do not break on old servers (via tolerant reader and neat defaults).
When do you do '/v2 '?
When invariants/semantics change, fields/methods are deleted, a new security model is required - it is easier and more honest to start a new line.
Can you live without Schema Registry/linters?
Theoretically - yes, practically - these are frequent regressions and "hidden" breakdowns. Automation pays off.
Enum can be extended?
Yes, if clients correctly handle unknown values (fallback/ignore). Otherwise - major.
Total
Contract compatibility is rules + discipline + automation. Design additively, version breaking changes, apply a tolerant reader, automatically check diffs and CDC, plan deprecate. This way APIs can evolve quickly and integrations can remain stable.