API versioning and contract compatibility
TL; DR
Compatibility is discipline, not luck. Keep a clear version policy (SemVer), change math (what "breaks," what doesn't), contract tests, schema registers, and Sunset procedures. For money and compliance - strict REST/gRPC with vN, for UI aggregations - evolutionary GraphQL with '@ deprecated'. Always: canary traffic, backward compatibility ≥ one release cycle, migration guides, field telemetry.
1) Basic concepts and goals
Backwards-compatible (BC): old clients are suitable for the new server.
Forwards-compatible (FC): new clients are suitable for the old server (limited).
Wire compatibility: the format on the "wire" does not break (especially important for gRPC/Protobuf).
SemVer: `MAJOR. MINOR. PATCH '- break the contract → raise MAJOR.
The goal is to minimize disruptive changes and provide predictable migration windows.
2) Change Matrix: What you can and can't
3) Policies for different API styles
3. 1 REST
Version in URI ('/v1/... ') or domain (' api-v1. '). Header version - for internal cases only.
Add, do not delete: new fields - ok, old - mark as' deprecated'in the diagram and leave for at least one cycle.
Statuses/errors: do not change the codes and structure'error. code/error. message/error. details`.
Idempotency is unchanged: do not turn a secure 'POST' with 'Idempotency-Key' into a 'behaviorally different' challenge.
3. 2 gRPC / Protobuf
Field numbers are sacred: do not reuse deleted numbers, mark as' reserved '.
Only adding new optional/repit fields; "hard mandatory" - through validation, not'required '.
Version packages: 'payments. v1`, `payments. v2`.
Service compatibility: new RPCs → a new method; we do not change the behavior of the old.
proto message Payout {
reserved 4 ;//field deleted, number reserved string id = 1;
string currency = 2;
int64 amount_minor = 3;
// v2: optional string comment = 5;
}
3. 3 GraphQL
Evolution without v2: add fields/types; deletion - through '@ deprecated (reason)' with the announcement of the window.
Persisted Queries: For public clients, use a whitelist of queries - it's easier to control compatibility.
Field-level authZ and telemetry: know which fields are actually used before deleting.
graphql type Payout {
id: ID!
amountMinor: Long!
currency: String!
comment: String @deprecated(reason: "Use note")
note: String
}
3. 4 Webhooks
Version in path ('/webhooks/v1/payments') and fixed event envelope ('event _ id', 'type', 'ts', 'data').
Keep signatures/NMAS unchanged; new algorithms - as an option with a flag.
Extensions - only through the new fields' data. 'and' headers' - without deleting the old ones.
4) Gateway API and version routing
Rules-based routing: by prefix '/v1 ', by header' X-Api-Version ', by domain.
Shadow/Canary: Reflect some of the production traffic on the new version "into the shadows," compare the answers.
Rate/Quotas per-version: Protects older clients during migration.
- 'Sunset:
'- version shutdown date - 'Deprecation: true '- version becomes obsolete
- `Link:
; rel = "deprecation" '- on changelog/migration guide
nginx location ~ ^/v2/ {
proxy_pass http://api_v2;
}
location ~ ^/v1/ {
add_header Deprecation "true";
add_header Sunset "Thu, 01 May 2026 00:00:00 GMT";
proxy_pass http://api_v1;
}
5) Scheme registers and contracts
OpenAPI / JSON Schema для REST; Protobuf descriptors для gRPC; SDL registry для GraphQL.
CI checks: linters + "breaking-changes check" in PR.
Consumer-Driven Contracts (CDC): Consumer tests (Pact/analog) - protection against inconspicuous breaks.
Changelog: machine-readable (for example, 'CHANGELOG. md '+ release notes in the registry).
6) Evolution of fields: rules of thumb
ID/keys: do not change the format (UUID↔int) without a new field '_ v2' and a transition period.
Time/currency: keep UTC ISO-8601/epoch and amount_minor + currency; do not scale (pennies/cents).
Enum: add values - ok; don't change the meaning of old ones. For REST, give string values, not ints.
Pagination: cursor-based more stable; do not change the semantics of the cursor.
7) Depletion and "Sunset" procedure
1. Announcement (T-90/60): changelog, mailing to partners, 'Deprecation/Sunset' headlines.
2. Duplicate period: V1 and V2 operate in parallel; V1 is equipped with warnings in responses/logs.
3. Usage telemetry: Who else calls V1? point contacts.
4. Freezing V1: only bug fixes/no feature.
5. Sunset-410 Gone or migration instruction block page.
8) Pain-free releases: laying strategies
Blue/Green or Weighted routing: 1-5-25-50-100% traffic.
Compatibility window: at least 1-2 minor releases, more often 6-12 months for external APIs.
Feature Flags to include new logic fields/branches without upgrading.
Read/Write-split: first add support for reading a new field, then start writing it.
9) Interoperability testing (practice suite)
Golden tests for responses from older versions.
Diff tests of circuits: no breaking in CI.
Replay production runs on staging for V2 (shadow).
Back/Forward scripts: new client on the old server and vice versa (where FC is valid).
Contract tests of webhooks: verification of signature, format, time.
10) Metrics and SLOs of the versioning process
% of customers on the last MINOR (target ≥ 80% before Sunset).
Compatibility/unavailability errors per release (target 0).
Share of legacy calls (decreasing to Sunset).
Client migration time (median/p95).
Latency/regression delta between versions (not worse than basic).
11) Examples of artifacts
OpenAPI (fragment, field depriction):yaml components:
schemas:
Payout:
type: object properties:
id: { type: string, format: uuid }
amount_minor: { type: integer }
currency: { type: string }
comment:
type: string deprecated: true description: "Use note"
note: { type: string }
Protobuf (reserved and v2 packet):
proto syntax = "proto3";
package payouts. v1;
message Payout { reserved 5; string id=1; int64 amount_minor=2; string currency=3; }
GraphQL (depletion):
graphql type Query { payout(id: ID!): Payout }
12) Versioning of adjacent channels
SDK/CLI: SemVer + API version dependency, compatibility stipulated in README.
Events/streams (WS/Kafka): version in'envelope. version`; new attributes - optional; dedup and resumes work the same between versions.
Reporting/CSV: version in file name/header; Add columns to the right do not change the order/types.
13) Governance and roles
Contract owner (domain owner), Steward API (rules and linters), Release Manager (Sunset/communications).
RFC process for breaking changes: business justification, migration plan, artifacts, dates.
Unified API directory: where diagrams, versions, Sunset calendar, contact are visible.
14) Anti-patterns
"Quiet" breaks: change the status/error/field type without a version.
Reuse of protobuf numbers - destroys replay and old clients.
GraphQL without field telemetry - touch removal.
Global v2 total - megamigration instead of point evolution.
The version in the query parameter for the public API is a non-obvious and vulnerable scheme.
There are no migration guides and examples - partners stall, deadlines are disrupted.
15) Check-list release of the new version
- Updated schema (OpenAPI/Protobuf/SDL), linters and breaking-checks passed.
- Integration and contract tests (CDC) added.
- SDK/sample code/migration guide and Changelog ready.
- Deprecation/Sunset enabled (old version) + How to migrate page.
- Canary/Shadow plan, alerts and dashboards comparing metrics.
- Backward compatibility is maintained for an agreed period.
- Rollback plan and risk matrix agreed.
Summary
A stable API is a process, not "once and for all." Live by the rules: SemVer + add-only evolution + circuit register + contract tests + Sunset procedures. Separate styles (REST/gRPC/GraphQL) and their policies, route versions to the Gateway API, roll out canaries, measure the effect. This way you will avoid "breaking surprises," accelerate partner integration and maintain predictability for monetary and compliance-critical domains.