# Evolve an API Without Creating a Version Graveyard

> Classify API changes by client-visible impact, then use telemetry, migration windows, and contract tests before you create a new version.

Canonical URL: https://www.devobs.io/articles/api-evolution-without-version-explosion/
By: Owen Park
Published: 2023-12-15T14:05:23.329Z
Updated: 2026-09-06T08:31:04.426Z
Section: Architecture

Create a new API version only when you must change behavior in a way existing clients cannot safely ignore or adapt to within the current contract. Most growth should be additive: new optional fields, new resources, and new operations. Compatibility is about observable behavior, not whether the server code was rewritten.

## Classify the client-visible change

Start with representative requests and responses. Adding an optional response field is usually compatible for clients that ignore unknown fields, but breaks strict decoders. Adding a required request field, removing or renaming a field, narrowing accepted values, changing units, reinterpreting null, altering ordering, or changing error semantics is usually breaking. Performance changes can also break clients when timeouts and [rate limits](https://www.devobs.io/articles/distributed-rate-limit-semantics/) are part of the practical contract.

Google's [AIP-180 backward compatibility guidance](https://google.aip.dev/180) treats existing client code as the compatibility audience and distinguishes source, wire, and semantic compatibility. It also explicitly warns against adding new required request fields and against removing or renaming existing components. Use a similar checklist for your protocol style. Do not label a change compatible merely because the JSON still parses; `status=done` changing from durable completion to queued acceptance is a semantic break.

HTTP already provides room for evolution. [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html) defines HTTP semantics and describes a uniform, extensible interface that enables independent evolution over time. Preserve those semantics. A new field does not require `/v2`, while changing an idempotent operation into a non-idempotent one cannot be repaired by keeping the same path.

## Design for tolerant growth

Document that clients must ignore unknown response fields and must not infer enums are closed unless the schema promises that. Servers can accept old request shapes while introducing new optional capabilities. Prefer explicit capability fields or a new operation over magic behavior inferred from unrelated headers. Preserve stable identifiers and let resources gain representations over time.

For enum growth, choose an unknown-value strategy before launch. [AIP-180](https://google.aip.dev/180) notes that code handling response enums may not tolerate new values gracefully even when the wire format change is additive. An `UNKNOWN` fallback, raw-string wrapper, or documented closed enum makes the risk explicit. For pagination, keep cursor semantics opaque; exposing offsets and sort internals makes later storage changes observable.

## Use telemetry before deprecation

A deprecation date without usage evidence is a wish. Instrument operation, client identity or SDK version where privacy permits, deprecated field use, and response variant. Publish the replacement, migration example, support window, and final behavior. Contact high-impact consumers and provide a test environment. Do not remove the old behavior until usage is below the agreed threshold or exceptions have owners.

[Contract tests](https://www.devobs.io/articles/contract-tests-for-apis-and-events/) should replay saved consumer expectations against both old and proposed implementations. Include unknown fields, omitted optionals, duplicate retries, stale conditional requests, pagination boundaries, and documented errors. Test popular generated clients because their behavior may be stricter than the protocol.

## Version the smallest necessary surface

If a semantic break is unavoidable, isolate it. A new operation, media type, or resource may be clearer than cloning the entire API under `/v2`. Full-version forks multiply documentation, authorization rules, observability, SDKs, and fixes. State how long both forms coexist and which data model remains authoritative.

Avoid compatibility layers that silently guess intent. If old clients send cents and new clients send decimal currency units, keep distinct fields or endpoints; heuristics based on magnitude will eventually corrupt data. Translate explicitly at one boundary and emit metrics for every legacy path.

Security behavior belongs in compatibility review too. Tightening authorization can be necessary even when a client relied on an accidental permission, but treat the rollout as a breaking correction: identify affected callers, provide the supported replacement, monitor denials, and keep the exposure window as short as the risk allows.

The next step is to take the next proposed API change and write five examples of existing client behavior before selecting a versioning mechanism. Add those examples as contract tests and a deprecation dashboard. Reviewed on 2026-09-06. Re-review if the API governance rules change.

## Source references

- <https://google.aip.dev/180>
- <https://www.rfc-editor.org/rfc/rfc9110.html>
