09. Versioning and Deprecation — The menu that changes without confusing regulars¶
~14 min read. Good APIs evolve gently, not by suddenly moving the kitchen.
Built on the ELI5 in 00-eli5.md. The menu — the familiar list customers trust — now teaches us how to add new choices without surprising loyal users.
First understand why API evolution needs discipline¶
APIs live longer than teams expect. Mobile apps, partners, scripts, and dashboards keep calling old behavior. If you change contracts casually, downstream systems break quietly. Updating the menu without warning confuses regular customers immediately. Backend API design must balance progress with trust. That is what versioning and deprecation manage. The hardest part is not syntax. The hardest part is compatibility thinking. Ask one question first. “What existing clients assume today?” That assumption map drives safe evolution. A simple picture looks like this: ┌────────────┐ ┌──────────────┐ ┌────────────┐ │ Old client │ ─→ │ API boundary │ ←─ │ New client │ └────────────┘ └──────────────┘ └────────────┘ Both clients may coexist for months or years. Your API should handle that reality. Now define terms carefully. Versioning is how you signal meaningful contract change. Deprecation is how you warn that old behavior will retire. Sunset policy is the timeline and rulebook for that retirement. Backward compatibility is the discipline that keeps old clients working. These are related, but not identical.
URL versioning and header versioning solve different problems¶
URL versioning puts the version in the path.
Examples look like /v1/orders and /v2/orders.
This is easy to see, easy to route, and easy to document.
It also helps debugging quickly.
Logs show the version plainly.
Gateways can route by path simply.
That visibility is why many teams start here.
Header versioning keeps URLs stable.
The client might send Accept-Version: 2 or media-type parameters.
Then /orders stays the same path.
This can feel cleaner conceptually.
The resource name remains stable while representation evolves.
But it is harder to inspect casually.
Caching and tooling may also need more care.
A simple comparison helps:
┌──────────────────────┬──────────────────────────────┐
│ URL versioning │ Header versioning │
├──────────────────────┼──────────────────────────────┤
│ visible in paths │ hidden in headers │
│ easy gateway routing │ cleaner resource naming │
│ simpler debugging │ more subtle client control │
└──────────────────────┴──────────────────────────────┘
Now be practical.
Choose one style and stay consistent.
The worst outcome is mixed strategy without reason.
If public partners and many tools are involved, URL versioning is often easier.
If representation negotiation is mature and clients are disciplined, header versioning can work nicely.
Worked example.
Suppose v2 adds split-payment details to orders.
With URL versioning, clients call /v2/orders/123 explicitly.
With header versioning, clients call /orders/123 plus a version header.
Both can succeed.
The real question is which model your ecosystem handles better.
Semantic versioning ideas help, but APIs need compatibility judgment¶
Teams often borrow semantic versioning language. Major versions signal breaking changes. Minor versions add backward-compatible features. Patch versions fix bugs without contract breaks. That framing is useful. But APIs are not libraries in one process. Client diversity makes compatibility much messier. So use semver ideas carefully. A breaking change is anything that can break a reasonable existing client. Removing a field is breaking. Renaming a field is breaking. Changing response type from string to object is breaking. Making a previously optional field mandatory can also be breaking. Now think about safe changes. Adding a new optional field is usually safe. Adding a new endpoint is usually safe. Adding a new enum value is only sometimes safe. Why only sometimes? Because clients may assume the old enum list is complete. That is the interview trap. A practical compatibility table helps: ┌──────────────────────────────┬────────────────────┐ │ Change │ Usually safe? │ ├──────────────────────────────┼────────────────────┤ │ add optional response field │ yes │ │ remove existing field │ no │ │ rename endpoint path │ no │ │ add optional request field │ usually yes │ │ tighten validation sharply │ often no │ └──────────────────────────────┴────────────────────┘ Now remember one subtle rule. The receipt can change shape only within the tolerance clients already have. If clients parse strictly, even harmless-looking additions can hurt. So document tolerance expectations clearly.
Deprecation headers and sunset policies create predictable exits¶
Deprecation is not a surprise announcement in chat.
It should be machine-readable and timeline-backed.
Useful APIs send clear signals in responses.
Common patterns include Deprecation and Sunset headers.
Some teams also include a link to migration docs.
That is excellent practice.
A response might communicate:
- this version is deprecated now
- support ends on a specific date
- migration guide lives at a given URL
This helps humans and automation.
Clients can detect retirement risk early.
Platform teams can measure who still uses old versions.
A simple sequence looks like this:
┌──────────┐ request v1 ┌──────────────┐
│ Client │ ───────────────→ │ API service │
└──────────┘ └──────┬───────┘
│ response headers
▼
Deprecation: true
Sunset: Wed, 31 Dec 2025 23:59:59 GMT
Link: migration guide
Now the policy question matters.
How long should a sunset window be?
Long enough for clients to migrate safely.
Short enough that the platform does not carry dead weight forever.
Public partner APIs may need many months.
Internal services may move faster.
The rule should match contract ownership realities.
Worked example.
Suppose 18 partner apps still use v1 orders.
You announce deprecation with a six-month sunset.
Monthly usage reports show which partners remain unmigrated.
Customer success and engineering can focus outreach intelligently.
That is much better than blind shutdown.
Backward compatibility rules prevent accidental betrayal¶
Versioning is not permission to be careless.
Even inside one major version, preserve trust deliberately.
Create compatibility rules and teach every team.
Examples of useful rules:
1. Never remove response fields in a live version.
2. Never reuse old field names for new meanings.
3. Prefer additive change over mutating existing semantics.
4. Keep old behavior behind flags until adoption is proven.
5. Publish migration guidance before enforcement dates.
These rules sound conservative.
Good.
Conservative is wise at API boundaries.
Now add observability.
Measure calls by version, endpoint, and client identifier.
Without usage visibility, deprecation becomes guessing.
Also keep contract tests.
A golden set of requests and responses can catch accidental breaks.
The wait staff analogy helps here.
They should not suddenly change table numbering on Friday evening.
Regular customers depend on habits.
APIs are no different.
Final worked example.
Suppose v2 of /payments returns status_detail as a nested object.
Old clients expect a string.
If you silently change v1, dashboards and webhooks may fail parsing.
If you introduce v2 separately, clients migrate intentionally.
That is boring, responsible evolution.
Exactly what mature platforms do.
Where this lives in the wild¶
- Stripe API engineer — introduces new capabilities carefully while preserving long-lived client integrations and publishing migration guidance.
- GitHub platform engineer — balances preview headers, version headers, and deprecation communication for a vast developer ecosystem.
- Shopify partner-platform engineer — manages sunset timelines so merchant apps can migrate without breaking storefront operations.
- Twilio backend API engineer — preserves compatibility across messaging products where customers depend on stable payloads and documented rollout windows.
- Internal platform engineer at PhonePe — tracks version adoption, sends deprecation notices, and protects old mobile clients during staged migrations.
Pause and recall¶
- Why are versioning and deprecation related but not identical concepts?
- What makes URL versioning easier operationally than header versioning sometimes?
- Which “small” response changes can still be breaking in practice?
- Why are observability and migration guides essential for deprecation success?
Interview Q&A¶
Q: When would you choose URL versioning over header versioning? A: When visibility, routing simplicity, and debugging speed matter strongly, especially for public APIs with many clients and tools. URL versions are explicit and operationally straightforward. Common wrong answer to avoid: “URL versioning is always outdated” — it remains very practical for many large API ecosystems. Q: Why is adding a new enum value sometimes a breaking change? A: Because some clients assume the old set is complete and fail on unknown values. Compatibility depends on consumer tolerance, not only provider intention. Common wrong answer to avoid: “Additive changes are never breaking” — consumer assumptions can make even additions unsafe. Q: What do deprecation and sunset headers accomplish? A: They make retirement timelines explicit in the protocol itself, so clients and tooling can detect upcoming change early. They convert vague announcements into actionable signals. Common wrong answer to avoid: “An email is enough” — emails help, but machine-readable API signals are much more reliable. Q: Why should backward compatibility rules exist even when you already use version numbers? A: Because versions do not excuse accidental breakage inside a supported contract. Teams still need guardrails for additive design, field stability, and migration discipline. Common wrong answer to avoid: “Just bump the version whenever needed” — that approach creates migration fatigue and ecosystem distrust quickly.
Apply now (5 min)¶
Imagine your orders API must add delivery-slot information next month. Decide whether v1 can handle it safely or needs v2. Write one reason for your choice. Then draft two response headers for a deprecated version. Add a sunset date and a migration link. Next, list three backward-compatibility rules for your team. Finally, name one metric you would track before shutting v1 down. Sketch from memory: draw one trusted menu, one v1 path, one v2 path or header, and arrows showing deprecation notice followed by sunset. Do not peek back.
Bridge. Versions reduce surprise, but clients still need precise failure messages and contracts next. → 10-error-handling-and-contracts.md