Skip to main content

Versioning & Backward Compatibility

Level 1: Level 1: Foundations & Mental Modelsmedium25 minversioningcompatibility

Prefer additive change with tolerant readers so you rarely version, use visible /v1 path versioning for true breaks, and retire versions with deprecate-warn-remove.

The best versioning strategy is to rarely need it

Versioning exists to let you change an API without breaking the integrations already depending on it. The core insight most engineers miss: the best versioning strategy is to need it as rarely as possible. Most changes should be additive and never trigger a version bump at all.

What breaks a client and what does not

Adding an optional field, adding an endpoint, adding an enum value that clients already ignore when unknown: non-breaking. Removing a field, renaming a field, changing a type, tightening validation, changing default behavior: breaking. If you design for additive change and your clients are tolerant readers (they ignore unknown fields and do not assume the response is exhaustive), the large majority of your evolution costs zero version bumps.

Check yourself

Your public API is at /v1 and your clients are tolerant readers. Sort each proposed change.

Add an optional 'gift_message' field to orders
Rename 'created' to 'created_at'
Add a new refunds endpoint
Tighten validation to reject addresses you previously accepted
Add a new value to the status enum

When you genuinely must break

  • URL-path versioning (/v1/orders, /v2/orders). Visible, trivial to route, trivial to test with curl, and easy for developers to reason about. This is the pragmatic default for public REST APIs (Stripe, Twilio, GitHub all expose a visible version).
  • Header or media-type versioning (Accept: application/vnd.acme.v2+json). Purer from a REST standpoint because the resource URL is stable, but it is invisible in a browser address bar, harder to test casually, and easy for a proxy to strip or ignore.

Per-paradigm nuance: GraphQL avoids URL versions entirely and evolves field by field, marking old fields @deprecated with a reason and adding new ones. gRPC follows Protobuf's field-number rules: add new fields with new tags, never renumber, mark removed tags reserved, so old and new binaries interoperate.

Compatibility runs two directions. Backward compatibility: a new server can still serve old clients. Forward compatibility: an old client can tolerate data from a new server (this is exactly what the tolerant-reader pattern buys you). You want both, because in a distributed deploy the two sides are never upgraded at the same instant.

Retiring a version is a sequenced migration

Deprecate (announce, document the replacement), warn (return Deprecation and Sunset headers, log usage, email the top callers), then remove only after telemetry shows traffic has drained. A hard cutover with no warning is how you generate an angry customer incident.

Interview nuance: the strongest signal is saying "I would design so most changes are additive and never bump the version, and only cut /v2 for a true break," then describing the deprecate-warn-remove sequence. Jumping straight to "put v1 in the URL" misses that versioning is a last resort.

Recap: prefer additive change with tolerant readers so you rarely version, use visible /v1 path versioning for true public breaks, and retire old versions with a deprecate then warn then remove sequence.

Check yourself
You genuinely must break the public orders API. Which rollout avoids the angry-customer incident?

Apply

Your turn

The task this lesson builds to.

Design a versioning strategy that lets you ship a breaking change to a public API without breaking existing integrations.

Think about

  1. URL-path vs header/media-type versioning, and which is the visible default?
  2. How do additive changes and tolerant readers avoid version bumps?
  3. How do you sequence a migration: deprecate, warn, remove?

Practice

Make it stick

A second problem on the same idea, so it survives past today.

Design a versioning model that lets you evolve a payments API for a decade while every integration written on day one still works (as Stripe has done since 2011, shipping changes constantly without ever breaking its API). Explain the mechanism and how new behavior reaches new callers without a /v2.

Think about

  1. What does pinning a version per account (rather than per URL) change about migration pressure?
  2. How can one canonical implementation serve a decade of historical response shapes?
  3. Why do coarse /v1-/v2 URL versions fail at a ten-year horizon?