SDSystem design
Start learning free

LESSON 25 OF 140 · ENGINEERING CRAFT

SYSTEM DESIGN · INTERVIEW WALKTHROUGH

API Versioning Explained: Breaking Changes, /v1 Paths, Deprecation

The problem: apps you can't update

Say you run a notes app for phones on a notes API: GET /notes/42 returns note 42 as JSON, and you ship a new version of the app every month. But updates are up to each user. Auto-update can be off, an old phone may not take the new version, and some people rarely go online, so some users are still on last year's app.

Every copy calls the same API. A web page loads fresh code on every visit, but a phone app's code is installed on the phone, so whatever the server sends, last year's code must understand it. That is the rule behind every versioning decision: old clients must keep working. An API that keeps that promise is backward compatible.

Safe and breaking changes

Adding a field: safe

The new app adds colored notes, so the server adds a field to every reply:

{"id": 42,
 "title": "Groceries",
 "text": "milk, eggs",
 "color": "orange"}

Last year's app, written before colors existed, reads the fields it needs by name:

note = get_json("/notes/42")
show(note.get("title", ""))
show(note.get("text", ""))

It never looks at color, so it shows the note as before. A client that ignores fields it doesn't know is a tolerant reader, and that habit is what makes adding fields safe. A strict client that rejects unknown fields would break on the first addition.

Renaming a field: breaking, and silent

Next the team renames text to body and ships a new app that reads body. Last year's app still asks for text, finds none, and shows the empty default: a blank note. The server answered 200 OK, so no error shows up in any log; users just see their notes go blank. That is a breaking change.

the reply has            new app shows   old app shows
id, title, text          milk, eggs      milk, eggs
id, title, text, color   milk, eggs      milk, eggs
id, title, body, color   milk, eggs      (blank)

The rule: add, never remove or change

  • Safe: adding a field to a reply ("pinned": true), adding an endpoint (GET /notes/42/history), accepting a new optional field in a request.
  • Breaking: removing or renaming a field, or changing its type. If "id": 42 becomes "42", a typed app may reject the whole reply, and in JavaScript "42" + 1 is "421". Making an optional request field required, or requiring a new one, breaks too: old apps don't send it and get 400 Bad Request.
  • Can break: a new value in a field clients switch on, such as a new color, "purple". An app that picks a paint for each color it knows may crash on one it has never seen, unless it was written with a default.

Renaming without breaking: send both

To get the clearer name anyway, add instead of renaming: send body beside text, holding the same words, and accept either name when a note is saved.

{"id": 42,
 "title": "Groceries",
 "text": "milk, eggs",
 "body": "milk, eggs",
 "color": "orange"}

Each app reads the name it knows. A "please update" field wouldn't help, since the old tolerant reader skips it too, and renaming the database column changes nothing, because apps only see the API's replies. The old name can go once no app reads it.

When you must break: a new version

Some changes can't be made by adding. GET /notes returns every note in one bare list, and some users have thousands. Sending a page at a time needs a pointer to the next page, and a bare list has no room for one:

[{"id": 42, …}, {"id": 43, …}]

{"notes": [{"id": 42, …}], "next": "p2"}

Last year's app loops over the reply as a list, so an object breaks it. The change ships as a new version beside the old one: old apps keep calling /v1/notes and get the list, new apps call /v2/notes and get pages. Switching everyone on a set date breaks the users who miss the message, and sending pages only to users with many notes breaks exactly those users.

In the path or in a header

GET /v2/notes

GET /notes
API-Version: 2
  • Path: visible in every link, log line and bug report, easy to try in a browser, and load balancers and caches tell versions apart by the URL alone. The cost: a note has a different URL in each version. Many public APIs do this.
  • Header (a custom one, or a media type in Accept): clean URLs, /notes in every version. But it's easy to forget: a request without it gets the default version, and a cache that stores replies by URL must be told to keep versions apart (the Vary header). GitHub's REST API, for one, takes its version in a header.

Keeping v1 cheap: a thin layer

Two versions with their own code means every fix is made twice. Instead, make v1 a thin layer over v2: it calls v2 and turns the reply back into the old shape. For the list, it collects v2's pages into the one list v1 always returned:

def v1_list_notes(user):
    notes, cursor = [], None
    while True:
        page = v2_list_notes(user, cursor)
        notes += page["notes"]
        cursor = page["next"]
        if cursor is None:
            return notes

A fix made in v2 reaches v1's callers too. Stripe has described doing this at scale, with small modules that turn the newest reply back into each older shape.

Deprecating and turning off v1

Deprecated means "still works, but going away". Each step needs the one before it:

  1. Announce it, in the docs and in a Deprecation header on every v1 reply.
  2. Count v1 calls per API key, to see who hasn't moved yet.
  3. Email the owners of those keys, with how to move to v2.
  4. Set a date once few keys are left, and send it in a Sunset header.
  5. Turn v1 off on that date, with an error that points to v2 (410 Gone is a common choice).

Here v1 was deprecated on November 1, 2026 (the header counts seconds since 1970) and turns off on May 1, 2027:

HTTP/1.1 200 OK
Deprecation: @1793491200
Sunset: Sat, 01 May 2027 00:00:00 GMT
Link: <https://notes.example.com/docs/v2>; rel="deprecation"

Last year's app can only show its generic error, which is why many apps ship a minimum-version check from their first release: a future server can then ask an outdated app to update.

Where it shows up in design interviews

Versioning is usually a sentence or two in the API step of the framework (Scope, Data, API, Busy path, Numbers, Services, Scale, Deep dives). Write the calls with a version in the path, GET /v1/notes/42, and say how they will change: as adds that old clients ignore, with any break shipped as /v2 beside v1. If the interviewer pushes, add that v1 becomes a thin layer over v2 and is retired by counting callers per API key, announcing a date, and turning it off.

Common interview questions

What is a breaking change?

Any change that stops an existing client working unless it changes too: removing or renaming a field, changing its type, requiring more in a request, or sending values old code doesn't expect. Adding fields and endpoints isn't one, as long as clients skip what they don't know.

How do you rename a field without breaking old clients?

Add the new name beside the old one, holding the same value, and keep both until no client reads the old one.

Path or header versioning: which would you choose?

The path, for most public APIs: it is visible in links and logs, and load balancers and caches handle it by URL alone. A header keeps URLs clean, but callers can forget it and caches must be told to vary on it.

How do you retire an old version?

Announce it with a Deprecation header, count its calls per API key, email the owners, set a date in a Sunset header once few are left, then turn it off with an error that points to the new version.

Summary

  • Old clients stay installed for years, so every change must keep them working.
  • Safe: adding fields and endpoints, read by tolerant clients.
  • Breaking: removing, renaming, changing a type, requiring more, unexpected values.
  • Versions: a break ships as /v2 beside /v1, and v1 becomes a thin layer over v2.
  • Deprecation: announce, count by API key, email, set a date, turn it off.
Keep going with system designInteractive lessons from the building blocks to Twitter, Uber and payments, the way interviews ask. Free to start.
Start learning free