Call
APIs

API Versioning Without Chaos: Breaking Changes, Deprecation, Sunset, and Migration

Version APIs only when compatibility actually breaks. Learn additive evolution, path/header/date versioning, client pinning, deprecation and Sunset headers, migration windows, observability, contract testing, and how to retire old versions safely.

Published: February 13, 2025
Updated: September 12, 2026
12 min read
API Versioning Without Chaos: Breaking Changes, Deprecation, Sunset, and Migration

API Versioning Without Chaos: Breaking Changes, Deprecation, Sunset, and Migration

API versioning is not about adding /v2 every time the backend changes. It is about giving clients a stable compatibility contract while the service continues to evolve.

A healthy API tries to make most changes backward compatible. A new major version should appear when the old contract cannot safely express the new behavior without breaking existing clients.

The difficult part is not naming the version. It is operating two contracts long enough for real consumers to migrate and then retiring the old one without surprises.

Start by defining what “breaking” means

A breaking change is one that can cause a previously valid client to fail, misbehave, or interpret data incorrectly without changing its code.

Common examples include:

  • removing an endpoint
  • removing or renaming a response field clients may use
  • changing a field's type
  • changing an enum's meaning
  • making an optional request field required
  • changing authentication requirements incompatibly
  • changing pagination semantics
  • changing error/status behavior clients rely on
  • changing a mutation from idempotent behavior to non-idempotent behavior

Not every internal code change is an API change.

A database migration, new service implementation, or performance optimization should be invisible to clients when the external contract stays the same.

Prefer additive evolution inside one stable version

Many useful API changes do not require a new version.

Usually safe additive changes include:

  • adding a new endpoint
  • adding an optional request field with safe default behavior
  • adding a response field when clients are expected to ignore unknown fields
  • adding a new filter/sort option
  • adding a new capability behind an explicit opt-in

Example:

{
  "id": "o42",
  "status": "paid",
  "total": 4999,
  "currency": "INR"
}

Adding a new receipt_url response property should not require /v2 if clients are built to tolerate additional fields.

This is why API compatibility rules should be documented for both server and client developers.

Client robustness is part of versioning strategy

A server cannot evolve additively if clients crash whenever an unknown field appears.

Clients should generally:

  • ignore unknown response fields
  • avoid depending on JSON property order
  • handle documented nullable/optional fields
  • treat new error details as additive

For enums, the situation is harder.

A client that assumes only:

pending
paid
cancelled

may break when the server adds:

refunded

Decide whether enum expansion is part of the compatibility contract and generate/client-code accordingly.

Do not expose patch versions in every request

An HTTP API usually does not benefit from URLs such as:

/v1.4.17/orders

Google's current API versioning guidance uses major versions such as v1 rather than exposing minor and patch numbers to API consumers.

The server can ship bug fixes and backward-compatible features in place while consumers remain on the stable major contract.

This separates software release version from API compatibility version.

Your backend may deploy 200 times while the public API stays v1.

Path versioning is obvious and practical

A common strategy is:

https://api.example.com/v1/orders
https://api.example.com/v2/orders

Advantages:

  • version is visible in logs and URLs
  • easy to route at gateways/proxies
  • straightforward documentation
  • clients explicitly choose the contract

Trade-offs:

  • version becomes part of resource URLs
  • teams can be tempted to duplicate entire APIs instead of sharing implementation
  • maintaining several large route trees can get expensive

Path versioning is often a good default for public REST APIs because its behavior is explicit.

Header versioning keeps resource URLs stable

Another pattern selects a version through a request header:

GET /orders/42
X-API-Version: 2026-09-01

Advantages:

  • resource URI stays stable
  • flexible per-request contract selection
  • date-based compatibility versions fit naturally

Trade-offs:

  • less visible when debugging/copying URLs
  • caches/gateways must vary correctly by version header
  • generic browser navigation is less obvious

If a header changes the representation, ensure caching infrastructure includes it in cache-key/Vary behavior as appropriate.

Query-parameter versioning is another valid option

Example:

/orders/42?api-version=2026-09-01

It is explicit and easy to test, though it mixes contract selection with ordinary query parameters.

Microsoft's REST API guidance has historically supported path and query-parameter versioning patterns.

Consistency across one API family is more important than arguing that only one mechanism can ever be correct.

Date-based versions can work well for continuously evolving APIs

Instead of v1, v2, some APIs let clients pin to a date:

2026-09-01

The date represents a known behavior snapshot.

This can work well when:

  • the provider releases changes frequently
  • consumers should upgrade on their own schedule
  • the platform can maintain compatibility behavior by requested version

The operational cost is real: the backend needs a clear compatibility layer and tests for supported version dates.

Do not expose arbitrary historical dates unless the service can genuinely reproduce those contracts.

Version the interface, not the database

Suppose a database column changes from:

full_name

to:

first_name + last_name

The API can still return:

{
  "name": "Asha Singh"
}

No API version is required if the observable contract stays compatible.

The API is an abstraction boundary precisely so internal implementation can evolve independently.

Keep version-specific translation near the boundary

Avoid copying the whole business system into v1 and v2 codebases.

A cleaner architecture is often:

v1 request adapter
       |
       v
shared domain/service logic
       |
       v
v1 response adapter

v2 request/response adapters
       |
       v
same core domain logic where possible

Only behavior that truly differs between versions should fork.

This reduces the chance that bug fixes reach v2 but not v1.

Breaking changes deserve an explicit migration path

Do not publish “v2 available” and immediately delete v1.

A migration plan should tell consumers:

  • what changed
  • why it changed
  • new version identifier
  • exact action required
  • deprecation date
  • sunset date if known
  • compatibility examples
  • test/sandbox availability

A useful migration guide compares old and new requests directly.

Example:

v1: amount is decimal major units
v2: amount_minor is integer minor units

Explain semantics, not only renamed fields.

Deprecation is not the same as shutdown

A deprecated API still works.

It means:

do not build new dependencies here; migrate existing ones.

RFC 9745, published in March 2025, standardizes the HTTP Deprecation response header field.

It can signal when a resource is or will become deprecated.

Example conceptually:

Deprecation: @1788134400

The header communicates lifecycle information without changing the resource's behavior.

Link to migration documentation

RFC 9745 also defines a deprecation link relation that can point clients toward human-readable migration information.

A response can include lifecycle metadata plus documentation explaining:

  • replacement endpoint/version
  • migration steps
  • compatibility differences
  • support contact

Runtime deprecation signals are especially valuable because they reach clients actually calling the old API, not only developers who happen to read a changelog.

Use Sunset for the planned shutdown date

RFC 8594 defines the Sunset HTTP response header.

It communicates the time after which a resource is expected to stop being available.

Conceptually:

Deprecation: @1788134400
Sunset: Tue, 01 Dec 2026 00:00:00 GMT

Deprecation and sunset solve different lifecycle stages:

deprecated → still works, migration recommended
sunset     → planned end of availability

Do not announce a sunset date earlier than the deprecation date.

Give consumers enough overlap

The correct migration window depends on the client population.

A first-party web frontend may deploy in minutes.

A mobile application can have older versions in the wild for months.

A public API may be embedded into customers' business workflows and require a much longer transition.

Choose overlap from:

  • consumer type
  • release cadence
  • contract severity
  • migration complexity
  • legal/enterprise support commitments

Do not pick a shutdown date only because maintaining v1 is inconvenient.

Measure version usage before retirement

Your gateway/API telemetry should answer:

requests by API version
unique active clients by version
critical customers still using old version
old-version error rate
specific deprecated endpoints/fields still used

Before sunset, identify remaining consumers and contact them where your customer relationship permits it.

A version with “only 0.1% of traffic” may still belong to one business-critical customer.

Traffic percentage alone is not migration readiness.

Identify clients safely

Useful client dimensions can include:

  • API key/client ID
  • OAuth application ID
  • first-party app version
  • SDK version

Avoid relying only on IP address or User-Agent, which can be ambiguous or spoofed.

Do not turn customer IDs into unbounded public metric labels; keep high-cardinality analysis in logs/analytics designed for it.

Contract tests should protect old versions

As long as v1 is supported, every backend release should prove v1 still behaves according to its contract.

Useful tests include:

  • request validation
  • response schema
  • status/error behavior
  • idempotency
  • pagination
  • authentication/authorization
  • version-specific semantics

Keep representative golden/consumer-contract tests for every supported API version.

Otherwise, “supported” old versions can silently break when shared domain logic evolves.

Test old SDKs against new servers

If you publish SDKs, compatibility is a matrix:

old SDK + current server
current SDK + old supported version

A server-side additive change can still break an SDK generated with overly strict enum parsing or schemas.

Run compatibility tests using actual older client releases, especially before changing validation or response behavior.

API version and SDK version are different

An SDK might be version 6.3.1 while speaking API v1.

The SDK version describes the library release.

The API version describes server contract compatibility.

Do not couple them unnecessarily:

SDK 6 does not automatically mean API v6

Document which API versions each SDK supports.

Avoid permanent preview versions

Alpha/beta/preview APIs exist so providers can iterate with looser stability guarantees.

They should have explicit expectations:

  • stability level
  • support level
  • breaking-change policy
  • graduation path
  • retirement policy

Google's current AIP guidance distinguishes stable versions from alpha/beta or date-based preview channels and requires deprecation/migration discipline appropriate to each stability level.

A “beta” API running unchanged for six years creates uncertainty rather than flexibility.

Do not version every endpoint independently without a reason

Fine-grained versioning can reduce unnecessary migrations, but it can also create a compatibility puzzle:

users v3
orders v1
payments v4
inventory v2

Sometimes that is correct for independently owned APIs.

Inside one cohesive public API, a clear major-version boundary is often easier for consumers.

Choose versioning scope from ownership and compatibility needs rather than maximizing granularity.

GraphQL handles versioning differently—but still has compatibility

GraphQL commonly prefers additive schema evolution and field deprecation instead of /v2/graphql for every change.

A typical path is:

add new field
mark old field deprecated
measure usage
migrate clients
remove after compatibility window

Breaking GraphQL changes still exist:

  • remove field
  • change type incompatibly
  • tighten nullability
  • change enum semantics

A single endpoint does not eliminate version management; it moves compatibility into schema evolution.

gRPC/Protobuf has its own wire-compatibility rules

For Protobuf APIs, field numbers are part of the wire contract.

Safe evolution often means:

  • add new fields with new numbers
  • never reuse removed numbers
  • reserve removed fields
  • avoid incompatible type/semantic changes

Major service/package versions may still be needed for intentionally incompatible contracts.

Do not apply REST URL-versioning rules blindly to every API technology.

Database and event schema versions are separate problems

An API migration can involve several layers:

public API v1/v2
internal domain model
message/event schema
stored database schema

These do not need to change at the same moment.

Use translation/adaptation boundaries so one API migration does not force a flag-day migration of every internal consumer and historical event.

Keep old versions secure during overlap

Version overlap means old code paths remain internet-facing longer.

Continue applying:

  • authentication fixes
  • authorization fixes
  • input validation
  • dependency/security patches
  • abuse/rate limits

Deprecated does not mean abandoned.

If maintaining an old version safely is impossible, communicate an accelerated migration rather than leaving known vulnerabilities exposed.

Avoid branching authorization rules by version unnecessarily

Changing data shape is one thing.

Allowing v1 to bypass an authorization rule fixed in v2 is usually unacceptable.

Keep security policy centralized and shared across versions where semantics permit.

Version adapters should translate compatible request/response shapes, not preserve security bugs as “backward compatibility.”

Use one source of truth for lifecycle metadata

Keep version lifecycle data in a machine-readable inventory:

version
status
release date
deprecation date
sunset date
replacement
documentation URL
owner

Use it to drive:

  • documentation banners
  • gateway headers
  • dashboards
  • customer notices
  • CI policies

This prevents documentation from saying one shutdown date while runtime headers say another.

A practical API lifecycle

A healthy major-version lifecycle can look like:

1. Stable v1

Backward-compatible features ship in place.

2. Breaking requirement appears

Document why compatibility cannot be preserved.

3. Build v2 adapters/contract

Reuse core business logic where possible.

4. Launch v2 while v1 remains supported

Publish migration guide and SDK support.

5. Announce v1 deprecation

Add runtime Deprecation metadata and documentation.

6. Observe migration

Track usage by client/customer/version.

7. Announce/confirm sunset

Use Sunset where appropriate and communicate directly.

8. Retire v1

Return a clear terminal response/documentation path after shutdown rather than failing mysteriously.

Production checklist

Before introducing or retiring an API version, verify:

  • compatibility rules are written down
  • additive changes stay in the current stable version
  • a new major version is justified by real incompatibility
  • the version selection mechanism is consistent across the API family
  • version adapters share domain/security logic where possible
  • migration docs compare semantics, not only field names
  • deprecation and sunset are separate lifecycle states
  • RFC Deprecation/Sunset metadata is used where useful
  • version usage is observable per client/application
  • old client/SDK compatibility is tested continuously
  • deprecated versions still receive security fixes during support
  • retirement decisions consider critical consumers, not only traffic percentage
  • lifecycle dates have one authoritative source

Good API versioning is mostly compatibility discipline and migration operations. The best version is the one clients barely notice because most evolution stayed additive—and when a breaking change finally requires a new contract, the migration is predictable rather than abrupt.

References

Tags:API VersioningRESTAPI DesignBackward CompatibilityDeprecationWeb APIs
ENTERPRISE SAAS ENGINEERING

BUILDING A SAAS PRODUCT?

Explore Lofingo’s end-to-end SaaS platforms, multi-tenant cloud architecture, and high-concurrency backend engineering.

EXPLORE SAAS SERVICES →