Backward Compatibility Specs: Deprecation Paths & Breaking Changes
You ship a new version of your API on a Friday afternoon. By Monday morning, three downstream teams have filed incidents because their integrations broke. The fields they relied on were renamed, an enum gained a new value their switch statements did not handle, and a previously nullable field now returns an empty string instead of null. None of these changes showed up in a diff as obviously dangerous. That is the problem with breaking changes: the ones that hurt most are the ones nobody thinks are breaking.
What makes a change "breaking"
A breaking change is any modification that causes existing consumers to fail without updating their code. The obvious cases are easy to spot: removing an endpoint, renaming a field, changing a URL path. But the subtle ones are where teams get burned.
Adding a new value to an enum is technically additive, but if a consumer deserializes it into a strict type or runs it through a switch statement with no default case, the consumer crashes. Changing a field from returning null to returning an empty string "" is not a schema change, but it breaks any null-checking logic downstream. Altering the semantic meaning of an existing field, like making created_at return UTC instead of the user's local timezone, does not change the type or name, but it changes every calculation that depends on that value.
Your spec should define what counts as breaking for your API. A good starting list: removing or renaming fields, changing field types, altering response status codes for existing behavior, modifying error formats, adding required request parameters, tightening validation on previously accepted input, and changing the semantic meaning of any field. Write this list down. Put it in your API versioning spec. When a proposed change matches anything on the list, the deprecation process kicks in.
The deprecation timeline spec
A deprecation without a timeline is just a wish. Consumers need dates, not vague warnings. The deprecation spec is a short document that tells every stakeholder exactly what is happening, when, and what they need to do about it.
Here is a template that works in practice:
# Deprecation Spec: GET /v2/users/:id response shape ## What is changing The `address` field (currently a flat string) will be replaced by a structured `address` object with `street`, `city`, `state`, and `postal_code` fields. ## Timeline - 2026-05-01: Deprecation announced. Both `address` (string) and `address_structured` (object) returned in responses. - 2026-06-01: Sunset header added to /v2/users/:id responses. Deprecation warning logged for consumers still reading `address`. - 2026-08-01: `address` field removed from /v2/users/:id. Only `address_structured` remains (renamed to `address`). ## Migration path Replace `response.address` with `response.address_structured.street` (or the full object). See migration guide: /docs/migrations/user-address-v3.md ## Support Contact #api-platform in Slack for migration assistance. Migration office hours: Thursdays 2-3pm PT through June.
The key parts: a concrete description of the change, a timeline with specific dates, a migration path with code-level guidance, and a support channel. Skip any one of these and you are making consumers guess. The timeline should give consumers at least one full release cycle, ideally two, to migrate. For external APIs, 90 days is a reasonable minimum. For internal APIs with a small number of known consumers, 30 days can work if you actively reach out to each team.
Specifying migration paths for consumers
Telling consumers "this field is deprecated" is not a migration path. A migration path tells them exactly what code to change and what the new code looks like. If you are deprecating a field, show the before and after. If you are changing an endpoint, provide the old call and the new call side by side.
For the address example above, the migration guide should include something like this:
# Before (v2, deprecated)
user = api.get("/v2/users/123")
street = user["address"] # "742 Evergreen Terrace, Springfield, IL 62704"
# After (v3)
user = api.get("/v2/users/123")
street = user["address_structured"]["street"] # "742 Evergreen Terrace"
city = user["address_structured"]["city"] # "Springfield"
state = user["address_structured"]["state"] # "IL"
zip = user["address_structured"]["postal_code"] # "62704"
Good migration guides also cover edge cases. What happens if address_structured is null for users who have not set an address? What about international addresses that do not have a state field? What does the consumer do during the overlap period when both fields exist? Answer these questions in the guide, not in a Slack thread three weeks after the deprecation notice goes out.
If the migration requires consumers to re-authenticate, update SDK versions, or change infrastructure (like switching from polling to webhooks), spell that out. The goal is that a developer reading the migration guide can complete the migration without asking anyone a question.
Versioning the deprecation itself
Deprecation is not just a note in a changelog. It should be machine-readable so that monitoring, client libraries, and CI pipelines can react to it automatically. Two standards are widely used for this: the Sunset HTTP header and the Deprecation HTTP header.
The Sunset header (RFC 8594) tells the client when a resource will become unavailable. It uses a standard HTTP date format:
HTTP/1.1 200 OK Sunset: Sat, 01 Aug 2026 00:00:00 GMT Deprecation: Mon, 01 Jun 2026 00:00:00 GMT Link: <https://api.example.com/docs/migrations/user-address-v3>; rel="sunset"
When consumers see a Sunset header, their HTTP clients or middleware can log warnings, fire alerts, or even block deploys that depend on sunsetting endpoints. The Link header with rel="sunset" points to the migration documentation. This turns deprecation from a human-only communication into something that tooling can enforce.
In your API spec, define which headers you will use, when they get added, and what the consumer's expected behavior should be. A contract test can verify that deprecated endpoints return the correct Sunset header and that the date matches the timeline in the deprecation spec. If the header says August 1 but the spec says July 1, someone made a mistake. The contract test catches it.
Communication specs: changelogs and notices
Having a deprecation spec is necessary but not sufficient. You also need to push the information to the people who need it. A changelog entry that says "deprecated the address field" does not reach the team that deployed their integration six months ago and never checks the changelog.
Your communication spec should define three things: what to communicate, when to communicate it, and through which channels.
What: Every deprecation notice should include the change summary, the timeline, a link to the migration guide, and a link to the support channel. Do not make consumers hunt for the details. One notice, all the information, every time.
When: Notify at announcement, again when the Sunset header goes live, and a final reminder 14 days before the sunset date. For external APIs, add a 30-day reminder. Three touchpoints is the minimum. If you are removing something that dozens of teams depend on, add more.
Channels: Changelog, API status page, email to registered API consumers, Slack or Teams message to known consumer teams, and in-response deprecation headers. A deprecation that exists only in a changelog is a deprecation that most consumers will miss. Meet them where they already are.
Track acknowledgment. If a consumer team has not acknowledged the deprecation notice within 14 days, follow up directly. Do not assume silence means they have read it and are handling it. Silence usually means they have not seen it.
When breaking changes are the right call
Not every change deserves a gentle deprecation path. Some changes need to happen fast, and a 90-day timeline would cause more damage than the break itself.
Security vulnerabilities. If an endpoint leaks PII or has an authentication bypass, you fix it now. You notify consumers after. A deprecation timeline for a security hole is a timeline for exploitation. Fix it, communicate the change, help consumers adapt. In that order.
Compliance requirements. When a regulation requires you to stop returning certain data by a specific date, the law sets the timeline, not your deprecation policy. GDPR right-to-deletion, PCI scope reduction, data residency requirements: these override your standard process. Document the regulatory driver in the deprecation spec so consumers understand why the timeline is compressed.
Unsustainable maintenance burden. If maintaining backward compatibility with a legacy endpoint requires your team to keep a deprecated database running, maintain two serialization formats, and test every release against a contract that no longer matches the system's actual data model, the compatibility tax may exceed the migration cost. In these cases, a shorter deprecation window with active migration support beats an indefinite maintenance commitment.
The key is to be honest about the trade-off. When you break compatibility fast, acknowledge the cost you are imposing on consumers. Provide dedicated migration support. Offer to pair with affected teams. The speed of the break is inversely proportional to the support you should offer.
Postmortem connection: what happens without deprecation specs
The postmortem of a breaking API change without contract tests documents what happens when a team ships a breaking change with no deprecation process. The response shape changed, no consumers were notified, no migration path existed, and three services failed in production.
The postmortem identified the root cause as a missing compatibility contract. The team had no spec defining what constituted a breaking change for their API. Without that definition, the engineer who made the change genuinely did not think it was breaking. They renamed a field from user_name to username to match the team's naming convention. From their perspective, it was a cleanup. From the consumer's perspective, it was an outage.
The remediation included adopting a backward compatibility spec, adding contract tests to CI, and implementing a deprecation checklist. All of these are things that could have been done before the incident. The cost of building the deprecation process after a production outage is always higher than the cost of building it before. You have the incident response overhead, the trust repair with consumer teams, and the rushed implementation under pressure.
If you are reading this article because you want to avoid that kind of incident, the starting point is straightforward. Define what breaking means for your API. Write it down. Add deprecation timelines to your spec template. Wire up Sunset headers. Run contract tests in CI. Build the rollout and rollback plan before you need it, not during the incident.
Topic Path
This article belongs to the API Contracts track. Start with the hub, then use the checklist, template, or tool below on a real project.
Keep reading
Fill a form, get a complete feature spec in Markdown — free, no signup.
Editorial note
This article covers backward compatibility and deprecation planning for API-driven software teams. Examples are illustrative engineering scenarios, not legal, tax, or investment advice.
- Author details: Daniel Marsh
- Editorial policy: How we review and update articles
- Corrections: Contact the editor
Consolidated Coverage
This canonical guide now covers several related notes that used to live as separate pages. Keeping them together makes Backward Compatibility Specs: Deprecation Paths & Breaking Changes easier to review, link, and use as the main reference.
- API Deprecation Plan Inside Your Contract
- Versioning Strategies for API Contracts
- Writing Backward-Compatible API Change Specs