Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Deprecation

Pattern

A reusable solution you can apply to your work.

“Nothing is so permanent as a temporary solution.” — Milton Friedman

Announce that a feature, endpoint, or field will be removed on a specific future date, keep it working in the meantime, watch who still uses it, and only remove it once the usage has actually gone to zero.

Also known as: Sunset, Deprecation Lifecycle

Understand This First

  • Parallel Change – deprecation is the lifecycle policy that governs the timing of an expand-contract migration.
  • Contract – you deprecate something because a contract needs to change, and the deprecation is how you honor the old contract until callers move off it.
  • Observability – you cannot safely remove a deprecated feature without watching whether anyone still depends on it.

Context

You own something other people use. It might be a public API, an internal library, a configuration key, a CLI flag, a database column, or a feature of your product. Whatever it is, removing it isn’t your decision alone. Every caller that still depends on it will break the moment it’s gone.

Sometimes the new design is clearly better. Sometimes the old design was a mistake from the start. Sometimes the underlying technology is being retired. In every case, the problem is the same: you need a way to get from “this exists and people use it” to “this is gone and nothing breaks” without a flag day that forces every caller to change at once.

Problem

How do you retire something that is still in use? You can’t rip it out on Monday and hope for the best. You also can’t leave it in forever, because then you’re maintaining two forms of the same thing and the cost compounds with every new feature that has to work with both. What you need is a disciplined way to signal the end, give callers a fair chance to move, watch whether they actually do, and only then finish the job.

Forces

  • Callers expect stability. A breaking change without warning destroys trust, even when the new design is better.
  • Maintaining two forms in parallel has a real cost in code, tests, documentation, and mental overhead.
  • Different callers move at different speeds. The first-party team that owns the replacement can migrate in a day; an external partner on a slow release cycle may need months.
  • Silent removal is worse than loud removal. A removed feature that was never announced as deprecated feels like an outage to the people who hit it.
  • Announcing a removal without a hard date just creates uncertainty. Callers defer migration indefinitely because nothing forces them to act.

Solution

Publish a deprecation notice with a specific sunset date, keep the deprecated thing working until that date, instrument it to see who is still using it, and only remove it once usage has dropped to zero or the sunset date has passed, whichever comes first. Deprecation is a four-part contract with your callers: an announcement, a grace period, visibility, and a hard ending.

The four parts:

Announcement. Say what is being deprecated, what replaces it, why, and when it will go away. The announcement goes everywhere callers look: release notes, API documentation, response headers, log warnings, compiler warnings, the library’s README. “Deprecated” without a replacement is just a complaint. Always name the alternative so callers know what to migrate to.

Grace period. Pick a window that is fair for the kind of caller you have. Internal code you own can move in a sprint. A library used by other teams in the same company typically gets one to three months. A public API with paying customers often gets six to twelve. The window should be long enough that a reasonable caller can plan and ship the migration, and short enough that it actually ends.

Visibility. Instrument the deprecated thing so you can see who is still calling it. For HTTP APIs, emit a Deprecation header and a Sunset header (per RFC 8594) on every response, and log each call with the caller’s identity. For libraries, use the language’s deprecation mechanism (Python’s DeprecationWarning, Rust’s #[deprecated], Java’s @Deprecated) so warnings show up at compile or run time. Build a dashboard that shows deprecated usage over time. This is the most important part, because it is what lets you tell whether the migration is actually happening.

Removal. When the sunset date arrives, check the dashboard. If usage is zero, remove the feature. If usage is nonzero but only from callers you can reach, chase them and reset the date. If usage is nonzero and you can’t reach the callers, you have a harder decision: extend the window, remove anyway and accept the breakage, or keep the deprecated thing forever. There’s no good answer if you skipped the Visibility step.

The reason deprecation works is that it turns a breaking change into a scheduled one. The people affected know in advance, they know exactly what to do instead, and they know when the window closes. The people removing the feature know whether it is safe to remove. Neither side is guessing.

How It Plays Out

A payments API has an endpoint called POST /charge that takes a currency amount as a string. Support has been fielding tickets about locale-related bugs for years, because "1,000.50" and "1.000,50" don’t parse the same way on every client. The team designs a new endpoint, POST /payments, that takes a structured object with an integer minor-units amount and an ISO currency code. They ship the new endpoint, add Deprecation: true and Sunset: Wed, 01 Oct 2026 00:00:00 GMT headers to every response from the old one, and post a migration guide linked from the API docs. A Grafana dashboard shows calls per day to POST /charge by API key. The first month, traffic drops by 30% as the biggest integrators migrate. The team sends targeted emails to customers still calling the old endpoint. Two weeks before the sunset date, only one customer remains: a hospital billing system on a slow release cycle. The team grants them a 90-day extension in writing. On the new date, the dashboard shows zero traffic. The team deletes the route in the next release.

A platform team inside a large company maintains a shared logging library used by fifty services. They want to remove a log.warn_once(key, msg) method that was always a mistake: it had a hidden global cache that leaked memory, and the semantics confused everyone. They add a @deprecated annotation with a message pointing at the replacement, log.warn(msg, dedup=key), and write a migration note in the library’s changelog. CI starts printing the deprecation warning on every build that still uses the old method. A static-analysis rule flags new uses in code review. The grace period is three months; the team tracks remaining call sites via a simple grep run in a scheduled job. By week six, only two services are still on the old method. A platform engineer opens pull requests against both of them with the mechanical migration. At the end of the window, they remove the method. No service breaks, because the platform team could see every caller and drive the last migrations themselves.

Tip

When directing an agent through a deprecation, give it all four parts as a checklist: announcement, grace period, visibility, and removal. Agents are happy to mark something @deprecated and move on, but without the visibility instrumentation nobody will know when the removal is safe. Ask the agent to add the deprecation warning and the logging and the dashboard query and the calendar reminder for the sunset date. Then, on the sunset date, ask a different agent to verify that usage is zero before removing anything.

A small engineering team uses an agentic workflow to refactor a configuration file format. The old format has a retries: 3 integer; the new format has retries: { max: 3, backoff: "exponential" }. They ask an agent to accept both forms during a deprecation window: when it sees the integer form, parse it, record a warning with the file path and line number, and continue. The agent ships the change. Two weeks later they grep the logs, find the remaining call sites, migrate them one by one (again with the agent), and wait another week with zero warnings before asking the agent to remove the legacy parsing code. The old format was never broken; it was gracefully retired.

Consequences

Benefits. Callers get a predictable timeline. Removal becomes a routine deletion rather than a risky change, because by the time it happens you already know nothing depends on it. The visibility instrumentation doubles as debugging data: you can see which customers are on which version of your API, which is useful for incident response and capacity planning. Public deprecation is also a trust signal. Teams that deprecate openly earn a reputation for not breaking things silently, which is worth real money when customers are choosing who to depend on.

Liabilities. Deprecation isn’t free. The deprecated thing has to keep working, which means bug fixes and security patches apply to both versions during the window. The visibility instrumentation is code you have to write and maintain. The sunset date is a commitment you have to remember — many teams have a graveyard of deprecated features that nobody ever actually removed, because the calendar reminder fell through the cracks and the pain of leaving them was lower than the pain of chasing the last caller.

The biggest failure mode is starting a deprecation and never finishing it. A deprecated thing that lives forever is worse than one that was never deprecated, because now your code contains both forms and a promise that one of them is going away. New contributors can’t tell which version to use. The old form accumulates bugs nobody fixes. The dashboard stops being watched. If you can’t commit to the removal step, don’t bother with the announcement.

  • Depends on: Parallel Change – deprecation is the lifecycle policy that says when the expand-contract sequence must finish. Parallel change is the how; deprecation is the when.
  • Depends on: Contract – you deprecate a feature because a contract needs to change, and the announcement is how you honor the old contract while it winds down.
  • Uses: Observability – visibility into who still calls the deprecated thing is the signal that tells you when removal is safe.
  • Uses: Metric – deprecated-call-rate over time is the metric that governs the removal decision.
  • Uses: Logging – per-call logging of deprecated usage, with caller identity, is the minimum instrumentation.
  • Enables: Refactor – deprecation is how you refactor a public interface without breaking callers.
  • Enables: Strangler Fig – a large-scale strangler migration is a deprecation applied to a whole system rather than a single feature.
  • Related: Migration – the grace period of a deprecation is when callers perform their migration.
  • Related: API – public APIs are where deprecation matters most, because their callers are outside your control.

Sources

Martin Fowler’s writing on evolutionary architecture and the Parallel Change bliki entry (2014) frames deprecation as the lifecycle wrapper around expand-contract. Most of the mechanics in this article (expand, migrate, contract) come directly from that line of work.

Sam Newman’s Building Microservices (2015, second edition 2021) develops the idea in a service-to-service setting, including the insight that you cannot safely remove a shared endpoint without observability into who still calls it. The combination of deprecation headers and usage dashboards is standard practice in that book.

The HTTP-specific mechanics (the Deprecation header from RFC 8594 and the Sunset header) were standardized by the IETF in 2019, drawing on years of ad-hoc practice from public API providers. Jennifer Riggins and the API-design community at Nordic APIs have documented the patterns that led to the standards.

Programming language deprecation mechanisms have a long history: Java’s @Deprecated annotation (Java 5, 2004), Python’s DeprecationWarning (PEP 565 and earlier), and Rust’s #[deprecated] attribute all encode the same idea. Mark the old form, warn at compile or run time, and let the ecosystem migrate before removal. The convergence across languages is itself evidence that the underlying pattern is stable.

Further Reading

  • RFC 8594: The Sunset HTTP Header Field – the standard for announcing a deprecation and sunset date in HTTP responses.
  • Martin Fowler, ParallelChange – the bliki entry that names the underlying mechanism.
  • Sam Newman, Building Microservices, chapter on breaking changes and consumer-driven contracts – a practical treatment of deprecation across service boundaries.