Evolutionbackward compatibilitybreaking changesadditive changetolerant reader

Backward Compatibility: The Real Rules

The safe list and the breaking list are shorter and stranger than intuition says. Adding an optional field is safe; making an optional field required is not; tightening validation, changing a default, or changing what a value means breaks clients without touching a single field name.

▶ Run the labFollow the failure

Frame the contract

API design starts with a consumer, a design question and a guarantee — never with a URL.

Design question
Which changes to this contract can ship today without breaking a single existing consumer — and which only look like they can?
Consumers
Every existing client, weighted by how slowly it updates: the browser app that redeploys hourly, the mobile build that lives 18 months in the field, the partner cron job written by a contractor who left. The slowest consumer defines what "compatible" means.
The promise
A contract with an explicit compatibility rule set lets the provider ship daily without fear and lets consumers integrate without defensive paranoia — both sides know exactly which changes can arrive unannounced.
RequirementConsumersResource ModelStyleContractValidationAuthorizationErrorsIdempotencyPaginationVersioningObservabilityEvolutionTrade-offs

The safe list and the breaking list

Compatibility has a direction. Backward compatible means new server, old clients: every request an old client sends is still accepted, and every response it receives is still one it can process. The lists below follow from that definition mechanically — a change is safe exactly when no old client's requests become invalid and no old client's response handling becomes wrong.

The asymmetry between requests and responses is the part intuition misses. On the *request* side you may loosen (accept new optional inputs, widen what validates) but never tighten — an old client cannot start sending a field it has never heard of. On the *response* side you may add but never remove or repurpose — an old client can ignore a new field (if the contract told it to), but code reading a removed field breaks, and code reading a repurposed field breaks *silently*, which is worse.

The lists, mechanically derived
ChangeVerdictWhy
Add optional request field / parameterSafeOld clients simply never send it; behavior without it must stay identical
Add response fieldSafe**Only if the contract requires tolerant readers — otherwise strict deserializers crash (see SDK Design: The Contract's User Interface)
Add new endpoint / operationSafeOld clients never call it
Add enum value the server returnsBreaking in practiceOld clients switch on the closed set and hit the default arm (see Enum Evolution: The New Value That Broke Old Clients)
Make optional field requiredBreakingEvery old request lacking it is now rejected
Remove or rename any fieldBreakingRename = remove + add; old readers and writers both break (see Removing Fields Without Removing Consumers)
Change a type (intstring, nullable → non-null response)BreakingDeserializers and null-handling written against the old type fail
Tighten validation (max length 500 → 100)BreakingRequests that succeeded yesterday fail today — a break with zero schema diff
Change a default or a field's meaningBreaking, silentlyNo client errors; clients compute wrong results. The most expensive row in this table

The breaks that never touch the schema

A schema diff catches perhaps half of real breaking changes. The other half are *semantic*: the shape is identical and the meaning moved. amount switches from cents to a decimal string of dollars. status: "completed" starts including refunded orders. The default page size drops from 100 to 25, and every unpaginated-by-laziness consumer silently loses data. Timestamps switch from server-local to UTC. An endpoint that returned items newest-first starts returning them oldest-first because an index changed — ordering nobody promised but everybody used, which is Hyrum's Law collecting its debt (see What an API Contract Actually Is).

Semantic breaks are the worst class because they fail open: no exception, no 4xx, no alert — just wrong numbers flowing into consumer systems. A duplicate-charge incident is loud and gets fixed in hours; a currency-unit change can corrupt a partner's books for a quarter before anyone reconciles. When you review a change for compatibility, the question is not "did the schema change?" but "could a client written against yesterday's behavior compute a different result tomorrow?"

Behavioral tightening deserves special paranoia. Rate limits lowered, timeouts shortened, previously-accepted garbage now rejected, authorization enforced where it accidentally was not — each is a *correctness improvement* for the provider and a breaking change for whoever depended on the slack. Sometimes you tighten anyway (the authorization case is not optional — see Authorization Design in the Contract); the discipline is knowing you are breaking someone and choosing it deliberately, with telemetry on who gets hit (see Consumer-Driven Evolution: Telemetry Before Breakage).

Zero schema diff, full break
1# Monday
2GET /orders/9 → { "amount": 1999 } # cents
3
4# Tuesday, "cleanup" deploy
5GET /orders/9 → { "amount": 19.99 } # dollars
6
7# No client throws. Every client that
8# compared, summed or invoiced amounts
9# is now wrong by 100×, silently.
Meaning change shipped as an addition
1# amount keeps its old meaning forever
2GET /orders/9
3→ {
4 "amount": 1999, # cents, unchanged
5 "amount_decimal": "19.99", # new, documented
6 "currency": "EUR"
7}
8# old field deprecated on its own timeline
9# (see [[removing-fields]])

A field's meaning is frozen the moment the first consumer reads it. New meaning gets a new name; the old name gets a deprecation process. Reusing the name saves one field and costs a silent, unbounded reconciliation incident.

Compatibility is a two-sided contract: the tolerant reader

The safe list only works if consumers hold up their half. "Adding a response field is safe" is true exactly when clients are required to ignore unknown fields — the tolerant reader rule. Write it into the contract on day one: *clients must ignore unknown response fields and must handle unknown values in extensible enums*. Without that clause, some consumer will deserialize with strict mode, and your first additive change becomes their outage — and contractually, it will be your fault or ambiguous, which is worse.

The same clause-writing applies to everything the safe list assumes: field order is not promised, error message text is not promised (branch on codes — see An Error Taxonomy Clients Can Branch On), response ordering is only promised where documented (see Sorting: Determinism or Drift). Every explicit non-guarantee is a change you can make later without a meeting. This is the cheapest evolution investment an API can make, and it only works if made before consumers integrate.

Verify mechanically, not by vigilance. Contract diffing in CI (an OpenAPI diff that fails the build on breaking changes — see OpenAPI: Describing the Contract, Not Designing It) catches the structural half; contract tests where consumers pin their expectations catch part of the behavioral half (see Testing the Contract, Not Just the Code). The semantic half — meaning changes — has no tool. It is caught by review culture that asks the Tuesday question: *what does a client written on Monday do with this response?*

An additive deploy, survived because the contract required tolerance
Request
GET /v1/orders/ord_812 HTTP/1.1
Authorization: Bearer <token>
Accept: application/json
Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "ord_812",
  "status": "processing",
  "amount": 1999,
  "fulfillment_eta": "2026-09-02",   ← new this release
  "carrier": null                     ← new this release
}

· 2023 mobile build: ignores both fields — contract said it must
· 2026 web client: renders the ETA
· zero coordinated deploys

Key points

  • Backward compatible = new server, old clients: old requests still accepted, old response handling still correct. Every rule derives from that.
  • Requests may loosen but never tighten; responses may add but never remove or repurpose.
  • Optional→required, type changes, tightened validation and lowered limits are breaking changes with no field renamed.
  • Semantic breaks — meaning, units, defaults, implicit ordering — produce no errors, only wrong results; they are the most expensive class.
  • The safe list requires a consumer-side clause written on day one: tolerant readers, unknown-enum handling, explicit non-guarantees.
  • Enforce structurally in CI with contract diffing; the semantic half is caught only by asking what Monday's client does with Tuesday's response.

Compatibility Analyzer

Change the contract and observe which guarantee moves.

Compatibility Analyzer
Eight proposed changes to a shipped API. Which break existing clients?
Add optional response field `nickname`
Rename response field `status` → `state`
Change `amount` from number to string ("19.99")
Server starts returning new enum value `suspended`
New optional request parameter `sort`
Make optional request field `currency` required
Return 422 instead of 400 for validation failures
Add a new endpoint `GET /projects/{id}/activity`

Follow the failure

How the contract fails or gets misused, hop by hop — and what it costs when it completes.

  1. 1
    Team → contract: ships v1 with no compatibility rules and no tolerant-reader clause; "we will be careful" is the policy.
  2. 2
    Team → deploy: adds a response field — the safest possible change; a partner's strict deserializer rejects the unknown key and their integration goes down.
  3. 3
    Team → overcorrection: freezes the response shape entirely; needed data gets bolted on through a second endpoint.
  4. 4
    Team → deploy: "fixes" amount from cents to dollars in place, since schema-wise nothing changed; no test fails.
  5. 5
    Partner → reconciliation: discovers a quarter of invoices off by 100× — the silent break outlived every loud one.
What breaks
  • Old clients fail on requests and responses that worked yesterday — mobile builds in the field cannot be hotfixed, so the breakage window is months, not minutes.
  • Silent semantic breaks corrupt downstream consumer data: books, dashboards and decisions built on wrongly-interpreted values.
  • The provider loses release velocity: after one bad break, every deploy needs a compatibility séance because no written rules exist to consult.

Design, observe, evolve

A contract decision is incomplete until you know how you would notice it failing and how it changes later.

Design the contract
  • • Publish the safe list and the breaking list as part of the contract, plus the consumer obligations (tolerant reader, unknown-enum handling) that make the safe list true.
  • • Never repurpose: new meaning gets a new field name, new behavior gets a new parameter or endpoint; the old one enters deprecation instead of mutating.
  • • Gate deploys on contract diffing in CI — a spec diff that classifies changes as safe/breaking and fails the build on unapproved breaks (see [[openapi]] and [[api-testing]]).
  • • State non-guarantees explicitly (ordering, message text, field order, timing) so behavior consumers should not depend on is contractually changeable.
Observe in production
  • • Segment error rates by client version / SDK version / API key after every deploy: a spike isolated to old clients is a compatibility break announcing itself (see [[api-metrics]]).
  • • Watch 400-response rates per endpoint after "validation improvements" — tightened validation breaks show up as rejected requests from unchanged clients.
  • • Semantic breaks surface in support tickets and reconciliation disputes, weeks late; treat any "the numbers changed" report as a possible in-place meaning change.
Evolve without breaking
  • • An API with tolerant readers and a written safe list evolves continuously: most quarters of product work ship as additive changes with zero consumer coordination.
  • • Changes outside the safe list are not forbidden — they are routed: through a new field ([[removing-fields]]), a new endpoint ([[api-migration]]), or a version ([[versioning]]), each with a consumer-movement plan.
  • • The rule set itself can strengthen additively (promising more) but weakening a promise consumers hold is itself a breaking change.
What it costs
  • • Never-repurpose accretes surface: `amount` and `amount_decimal` coexist for years, and every reader of the API sees the scar tissue.
  • • Strict CI contract-gating occasionally blocks changes that are technically breaking and practically harmless — the override path must exist, and every override is a small bet.
  • • Tolerant-reader clauses push work onto consumers (lenient parsing, forward-compatible enum handling) that strict schemas would have caught for them at compile time.

Misconceptions

Claim
“If the JSON schema did not change, the change is compatible.”
Reality
Schema diffs catch structural breaks only. Units, meanings, defaults, validation bounds, limits and implicit orderings all break clients with an identical schema — and they break silently.
Claim
“Adding a field is always safe.”
Reality
Adding a *response* field is safe only under a tolerant-reader contract; strict consumer deserializers reject unknown keys. Adding a *required request* field is always breaking. The verb "add" is doing a lot of hiding in that claim.
Claim
“We fixed a bug — clients depending on the bug are on their own.”
Reality
Contractually convenient, operationally false. If the buggy behavior shipped long enough to be depended on, fixing it is a breaking change and deserves the same telemetry and notice (see Consumer-Driven Evolution: Telemetry Before Breakage). You may still fix it — knowingly.

Apply it