Skip to content

API Versioning & Evolution#

Problem statement (interviewer prompt)

Design the versioning policy for a public REST API with 500 enterprise customers. New endpoints + new fields ship monthly; once a year you need a breaking change. Pick URI vs header vs evolutionary versioning, write the sunset / deprecation policy, and explain how SDKs migrate.

flowchart LR
  V1[v1 contract]
  V2[v2 contract]
  V3[v3 contract]
  V1 -- coexists --> V2
  V2 -- coexists --> V3
  V1 -. deprecated, sunsets after window .-> X([retired])

  classDef p fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
  classDef s fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
  class V1,V2,V3 s;
  class X p;

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
    class V1,V2,V3,X service;

Public APIs live forever. Version them so you can evolve without breaking clients: ideally additive changes only; if breaking, run multiple versions side-by-side and sunset older ones predictably.

Compatibility kinds#

Backward compat Forward compat
Definition new server can read old client new client can read old server
Example optional field added client tolerates unknown fields
Required by always usually

Versioning strategies#

flowchart TB
  subgraph URI[URI versioning]
    A1["/v1/orders"]
    A2["/v2/orders"]
  end
  subgraph Header[Header versioning]
    B1["Accept: application/vnd.x.v2+json"]
  end
  subgraph Param[Query param]
    C1["?version=2"]
  end
  subgraph Evol[Evolution - no version]
    D1[Add fields only<br/>tolerate unknown<br/>never break]
  end

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
    class A1,A2,B1,C1,D1 service;
Strategy Pros Cons
URI /v1/... obvious, cache-friendly, easy mental model URL churn, multiple impls
Header Accept clean URLs, content-negotiated hard to test in browser, less visible
Query param trivial to test not idiomatic, cache-noisy
Evolutionary (no version) no breaking changes, single codebase slower change cadence

Most public APIs default to URI versioning; gRPC + Protobuf use evolution.

What's safe to change#

Change Safe?
Add new optional field yes
Add new endpoint yes
Add new enum value yes - clients must tolerate
Rename field breaking
Remove field breaking
Tighten validation breaking
Loosen validation safe
Change semantic of existing field breaking

Sunset window#

sequenceDiagram
  participant A as v1 clients
  participant Srv as Server
  participant B as v2 clients
  Note over Srv: v2 launched
  A->>Srv: /v1/...
  Srv-->>A: 200 + Sunset: Wed, 30 Jun 2027 + Deprecation: true
  Note over A: client devs migrate
  B->>Srv: /v2/...
  Srv-->>B: 200
  Note over A,Srv: 12 months later
  A->>Srv: /v1/...
  Srv-->>A: 410 Gone
  • Announce deprecation; emit Deprecation: true + Sunset: headers (RFC 8594).
  • 6-12 month window typical for paid customers; 3 months for SDK-owned clients.

gRPC + Protobuf rules#

  • Don't reuse field numbers.
  • Don't change field types.
  • New fields → next free tag, with defaults.
  • Removed fields → reserve the tag number forever.
  • Reorganise messages only inside the same package version.

REST: error contract#

  • Use a structured error envelope (Problem Details RFC 7807):
    { "type": "https://errs/insufficient_funds",
      "title": "Insufficient funds",
      "status": 422,
      "code": "INSUFFICIENT_FUNDS",
      "trace_id": "abc-123" }
    
  • Stable codes + stable types - adding new codes is OK if old clients already handle "unknown".

Pagination, IDs, filters#

  • Pagination: cursor-based, opaque. Stable across additions.
  • IDs: prefix to identify type (ord_abc123) - easy to grep, hard to confuse.
  • Filters: support a stable subset; new filters add to the spec, never re-purpose.

Anti-patterns#

  • "v2 means v1 is dead today" - clients can't react that fast.
  • Breaking semantics in a "patch" - never.
  • Multiple versions with shared mutable state - fix bugs in one and not the other.
  • Header version negotiation behind a CDN that strips headers.

Glossary & fundamentals#

Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.

Tag Concept What it is Page
HLD CDN edge caching for static assets cdn
HLD CRDTs commutative replicated data types crdts
HLD API versioning & evolution URI / header versioning, sunsetting api-versioning-evolution
LLD REST API design verbs, statuses, pagination, errors rest-api-design
LLD Behavioural patterns Strategy, Observer, State, Command, Chain behavioral-patterns

Quick reference#

Practical playbook#

  • Default to evolutionary changes for as long as possible.
  • Bump major version only when behaviour changes or fields are removed.
  • Run N + N-1 in production; never N-2.
  • Track per-customer adoption per version; only sunset once usage is low.

Tooling#

  • OpenAPI / gRPC contracts in a versioned repo.
  • Schema diff tools (oasdiff for OpenAPI, buf for Proto).
  • Contract tests (Pact) run on PRs to surface accidental breaks.

Deprecation comms#

  • Deprecation: true + Sunset: <date> response headers (RFC 8594).
  • API portal banner + changelog feed.
  • Email customers at T-90, T-30, T-7 days.

API governance at scale#

  • One review group for breaking changes.
  • Per-team API owners.
  • Lint rules in CI for forbidden changes (renames, type changes).

Refs#

  • Stripe API versioning (date-based versions; legacy + adapter pattern).
  • GitHub API media-type versioning.
  • Google AIPs (API improvement proposals).
  • "API Design Patterns" - JJ Geewax.
  • RFC 8594 (Sunset header).

FAQ#

What are the common API versioning strategies?#

URI versioning like /v1/users, header versioning with Accept or a custom header, query parameter versioning, and evolutionary versioning that avoids breaking changes by adding fields only.

URI versioning vs header versioning, which is better?#

URI is simpler, discoverable in logs, and easy to cache. Header versioning keeps URLs stable and is favored by hypermedia purists. Most public APIs at scale pick URI for operational simplicity.

What counts as a breaking change in an API?#

Removing or renaming a field, changing a field type, tightening validation, changing an error code semantically, or reordering positional arguments are breaking. Adding optional fields or endpoints is not.

How long should you support an old API version?#

Public APIs typically support at least 12 months after deprecation announcement with a sunset header. Enterprise contracts may extend that to 24 months or longer depending on SLA commitments.

How do you handle API evolution without versioning?#

Use tolerant readers, additive changes only, feature flags per client, and Postel's principle. Many large APIs like Stripe pin clients to a version on first call and run multiple versions side by side.

  • API Gateway: API gateways enforce versioning policies and route traffic across API versions
  • HTTP Protocols: HTTP semantics and headers underpin all API versioning strategies

Further reading#

Curated, high-credibility sources for going deeper on this topic.