TL;DR - Platform teams write down how to build a paved road and never write down how to close one, so closing one is improvised, and it stalls. The failure mode is specific and almost universal: you announce the deprecation, sixty percent of the estate moves in the first two months, the tail does not move at all, and you now operate two roads forever. That is strictly worse than never having announced, because you pay double maintenance and your successor road stops being the obvious default. A retirement is a product launch in reverse and needs the same apparatus: a destination that already works for the workload class you are evicting, a migration you wrote and tested yourself, an inventory of who is actually on the old road, a named person funded to move the last twenty percent, an escalating date with a brownout before it, and an ending you delete rather than archive. Design path one so it can be closed, and most of this gets much cheaper.
I build and run platforms for a living, and I have written a fair amount about paving roads: the first one, and then the second and third, where the problem becomes portfolio discipline. What I have never written, and what almost nobody writes, is the other end of the lifecycle. The road you built in 2023 that runs on a base image nobody supports, a CI system you are leaving, and a deploy model you now think was a mistake.
It gets no attention because it is not a launch. There is no demo, no adoption story, no slide with a green line going up. It is the least glamorous work a platform team does and it is where a surprising amount of platform credibility is won and lost, because the road you failed to close is the road a new joiner will find in six months and build on. It is also the test of whether you meant it when you said the platform is an internal product, because products have end-of-life processes and internal tools have a shrug.
The half-migration is the expensive outcome
Start with the failure, because it is so common it may as well be the default.
Someone decides the old path has to go. An announcement goes out. There is a wiki page describing the new way. Enthusiastic teams move quickly, because enthusiastic teams always move quickly, and the number climbs to something like sixty or seventy percent within a couple of months. Then it stops. The remaining services belong to teams that are mid-quarter, or teams that no longer exist, or one compliance-critical box that nobody wants to touch. Twelve months later the old road is still there, still running, and quietly still being used.
Look at what that state actually costs you:
- You maintain two roads and get the benefit of neither. Every CVE in the base image is now two fixes. Every improvement to a primitive lands twice, and eventually lands once, in whichever copy someone remembered. This is paved-road sprawl arriving through the back door, produced by a decision that was supposed to reduce the path count.
- The default at the moment of creation goes ambiguous. An engineer scaffolding something new now sees two roads, one marked deprecated in a doc they have not read. Ambiguity at that moment is fatal, because almost all adoption is decided in the first ten minutes of a new thing existing.
- You have taught the org that your deprecations are optional. This is the part that compounds. The next time you announce an end of life, the teams who waited it out last time will wait it out again, and they will be right to.
- The old road rots while it is still load-bearing. Nobody staffs maintenance for a thing that is going away, so the deprecated path stops getting patched on the day of the announcement while continuing to carry production traffic for another two years. The riskiest system in a lot of estates is the one that was declared dead and never buried.
So the honest framing is this: a deprecation that stalls at eighty percent has cost you more than never starting. Which means the decision to announce is not a decision to stop recommending something. It is a commitment to finish, and you should make it with the same seriousness you would apply to committing to build something.
A retirement is a launch, in reverse
When you launched path one, you did product work. You found a design partner, you removed friction in hours rather than sprints, you told the story, you supported it when it broke. A retirement needs exactly the same apparatus pointed the other way, and teams consistently budget a tenth of the effort because “it is just a migration”.
Here is my gate. You may not announce a deprecation until all six of these are true:
- The destination exists and is better for this specific workload class. Not better in general. Better for the things you are about to evict. If the old road serves batch jobs and the new one is tuned for HTTP services, you are not deprecating, you are evicting people into a worse home and they will notice within a week.
- You have migrated at least one real production service yourself, end to end. Ideally one of your own, and otherwise one with a friendly owner. Until you have done it, the migration guide is a hypothesis. Everything genuinely painful about the move is invisible from the platform team’s side of the fence.
- The migration is a diff, not a document. A wiki page that says “update your pipeline to use the v2 workflow and set the new resource annotations” is a request for twelve teams to each independently rediscover the same six gotchas. A script, a codemod, or an opened pull request is a migration. This is the same arithmetic as ordinary adoption: the cost is paid now, by them, and the benefit lands later, mostly on you.
- You know the size and shape of the estate. Not roughly. A list, with owners. More on this below.
- A person owns the retirement, by name, with time allocated. Not the team, not the rota. A retirement with no owner is a retirement that becomes everyone’s third priority.
- The tail is funded before the announcement, not after. Also below, because it is where these things die.
If you cannot make all six true, you can still stop recommending the old road, stop creating new things on it, and quietly stop investing. What you should not do is announce a date you have not resourced. A deliberate not-yet is a much better artefact than a stalled campaign.
Count the estate before you announce
You cannot retire what you cannot enumerate, and most platform teams discover at exactly the wrong moment that they have no idea who is on the old road.
Where the list comes from, in rough order of how much I trust it:
- Provenance markers in the repo. If the template stamped a
platform.path=service-v1file or label into every repo it generated, this takes an afternoon. If it did not, add that to your current paths today, because you are writing this exact post for your future self. - What the primitives see. Which repos call the shared pipeline definition, which images pull the old base layer, which services register with the old deploy mechanism. Runtime evidence beats a catalogue, because it only shows things that are actually alive.
- The catalogue, treated as a hint. Service catalogues are usually stale in the direction that matters here: they under-report the abandoned and over-report the tidy. Useful for owner lookups, not for counting. Which is one more reason a portal is not the same thing as a platform.
Then sort what you find into four buckets, because they need four different treatments:
- Alive and actively developed. The easy ones. These teams are in the code weekly, the migration lands in a normal sprint, and a pull request from you will usually be merged.
- Alive but frozen. Running in production, nobody has touched it in eight months, the owning team has moved on to other things. These are the ones that turn into the tail. They have an owner on paper who does not think about this service at all.
- Dead but still running. Nobody uses the endpoint, nobody watches the dashboard, the deploy is from last year. Do not migrate these. Turn them off. A retirement is the best excuse you will ever get to delete things, and every service you delete is one you do not have to move, patch, or pay for.
- Genuinely orphaned. No team, no owner, still serving traffic. Escalate these early, in writing, as a separate risk item. They are not a migration problem, they are an ownership problem that the deprecation happened to uncover.
The number I would report is not percent migrated. That is the same slowly-moving portfolio fact I would keep off a dashboard in any other context. Report the count of remaining services and the count of distinct owning teams, because those are the two numbers that tell you how much conversation is left, and the second one usually collapses much faster than the first.
Somebody has to pay for the last twenty percent
This is the whole post, honestly. Everything else is logistics.
The distribution is remarkably consistent. Around sixty percent of an estate moves on a pull request and a nudge. Another twenty moves after one real conversation with a team that had to schedule it. The final twenty percent is frozen services, teams under delivery pressure, teams that were reorganised out of existence, and the one system where the migration is genuinely hard. That tail does not respond to reminders, and it will not respond to the eleventh reminder either.
There are exactly three honest ways to pay for it, and you pick one per team, before you announce:
- The platform team does the migration. You write it, you test it, you ship it, you carry the risk of the change. This is expensive and it is frequently correct, because the retirement is your project and the benefit is mostly yours. Budget it as the cost of the road’s whole life, not as a favour. If that number frightens you, that is useful information about the paving bar you applied when you built the thing.
- The owning team gets a funded, scheduled slot. Their manager agrees a specific window, in writing, before the announcement goes out. Not “they will get to it”. A date on a plan that somebody with the authority to move work has agreed to. Getting this for the six teams that matter is two weeks of unglamorous conversation and it is worth more than any amount of tooling.
- An explicit, expiring exception with a named risk accepter. Sometimes the answer really is that this service stays on the old road until it is decommissioned in Q3 next year. Fine. Write it down as an exception with an expiry date and a person who owns the residual risk, the same way a security exception should be a first-class artefact rather than a silence. Then you know precisely what carrying the old road is costing you and until when.
There is a fourth option, which is the one almost everybody actually takes: we will chase them. It is not a plan. It is a way of converting a retirement into a permanent low-grade nagging obligation that eventually stops, at which point you have a zombie path with an official-looking deprecation notice on it.
The date is a product decision
Once the tail is funded, pick the window, and pick it deliberately, because both errors are real.
Too short and you have written a mandate with a deadline attached to a road teams have not yet had time to trust. Too long and you get the worst version of human nature: nothing happens for eleven months, everything happens in the twelfth, and you paid double maintenance the whole time for a queue that formed at the end anyway. My rule of thumb is that the window should be roughly the time the migration takes for your slowest realistic team, multiplied by two or three, and not rounded up to a calendar year because a year sounds generous.
Then run it in stages, because a single date is a cliff and stages are a slope:
- Block new creation on day one. This is the highest-leverage lever in the entire retirement and it costs almost nothing. Remove the old template from the index, remove it from the scaffold command, remove it from onboarding docs, and make creating something new on the old road require a deliberate act. Stop the bleeding first. Every service created after the announcement is one you will personally migrate later.
- Warn where the work happens. A deprecation notice in the pipeline output of every affected build, naming the successor and linking the migration, is worth fifty Slack messages. Developers read their own build logs. They do not read your announcements.
- Brown it out before you kill it. Turn the old path off for two hours on a Tuesday morning, announced a week in advance. This is the single most useful thing in this post after funding the tail. It converts “we think these fourteen services are still on it” into a fact, it flushes out the users your inventory missed, and it makes the discovery happen on a weekday with everyone awake, rather than at midnight in the final week. It is the same logic as any deliberate failure test: a system you have never seen break under controlled conditions is a system you are guessing about.
- Block deploys before you block running. A service that cannot deploy on the old road but keeps serving traffic gets attention immediately and breaks nothing for users. It is the gentlest forcing function available.
- Then actually turn it off, on the date you said. Once, credibly, so the next deprecation is believed.
This is also the one place in platform work where compulsion is legitimate, and it is worth being precise about why. Elsewhere I would argue hard that routing around your road is a product signal rather than a compliance problem. A dated retirement is the exception the same way a datacentre exit is: the destination already exists, the migration is written, the tail is funded, and the only remaining question is scheduling. Mandate the how, never a destination that does not exist yet.
Turn it off loudly, then delete it
The last five percent of a retirement is where zombie paths are actually born, because the work is boring and nobody is watching any more.
- Delete the template, do not archive it. A read-only copy kept “just in case” will be found, cloned, and put into production by someone with a deadline. If you must keep it, keep it somewhere that cannot be cloned into a working pipeline.
- Kill the docs the same day. Redirect the old page to the new one, or put a loud banner naming the successor at the top. Stale internal documentation is a competing product with excellent internal search ranking, and it will outlive you.
- Remove the infrastructure, not just the entry point. The CI job, the runner pool, the old registry, the DNS entry, the IAM role, the storage bucket that has been billed monthly since 2023. This is the part where a retirement pays for itself in the bill you stop generating, and it is the part most often left undone because the interesting work finished two weeks earlier.
- Write the ending down. One page: what the road was for, what replaced it, how long the retirement took against the estimate, what made the tail hard, and what you would build differently. Ten minutes, and it is the only artefact that makes the next retirement cheaper. A document that changes the next decision is the only kind worth writing, which is the whole argument I made about postmortems that actually change something.
- Tell people it is done. Retirements are invisible work, and invisible work does not get funded twice. “The v1 service path is gone, 34 services moved, here is what it cost and here is what we deleted” belongs in the changelog next to your launches.
Build path one so it can be closed
Most of the cost of a retirement is decided years earlier, at construction time, which means the cheapest thing you can do about the road you will retire in 2029 is to change how you are building the one you are shipping this quarter.
- Keep the template thin and the primitives fat. The less a path copies into each repo, the less there is to migrate. If the real value lives in a shared pipeline definition, a base image, and a library, you can often replace the road underneath people and nobody has to do anything. The best retirement is one your users never notice, and it is only available to teams who put their opinions in the primitives rather than in the scaffold. This is the reuse rule from the second and third path, cashed in years later.
- Stamp provenance from day one. Which path generated this, which version, when. It costs one file and it turns your future inventory from archaeology into a query.
- Version the path and expect to move people between versions. A team that has already been through two painless minor migrations will believe you about the third. The muscle is built on small moves, and if the last three changes cost them nothing they will not brace against the next one.
- Keep the escape hatch documented and honest. Knowing where people went off-road is knowing where your users are. If off-roading is unofficial, your inventory is fiction.
- Track usage while it is boring. A weekly count of services per path, recorded from the beginning, costs nothing and means the retirement conversation starts with evidence instead of a survey.
None of that removes the work. It changes a twelve-month cross-org campaign into a two-month piece of maintenance, which is roughly the difference between a retirement that finishes and one that does not.
When this is the wrong answer
The honest off-ramps, because plenty of paths should not get this treatment.
- The path has no users. Check properly, then just delete it. Running an announcement, a window, and a migration programme for a road with zero services is ceremony, and it teaches people that your deprecations are theatre. Mention it in the changelog and move on.
- You want it gone for taste, not for cost. Work out the actual carrying cost of the old road: maintenance hours, security exposure, infrastructure spend, cognitive load on new joiners. If it is cheap, stable, patched and quiet, then migrating nine teams to make your portfolio elegant is spending other teams’ quarters on your aesthetics. Retire what is unsafe, unsupportable or expensive, not what is unfashionable. The boring option deserves the same respect when it is old as when it is new.
- The destination does not exist yet. Never announce an end of life into a road you are still building. You will get twelve improvised variants in a fortnight, all of them now permanent, and you will have converted one deprecated path into a dozen unpaved ones.
- The forcing function is external and dated. A runtime end of life, a datacentre exit, a certificate authority disappearing, a regulator with a deadline. That is not a deprecation you are choosing, it is a migration project with a hard date, and it needs project governance: a staffed programme, cross-team tracking, and someone senior who owns the date. Running it as a platform-team side quest between tickets is how those go wrong.
- You are eight engineers with four services. There is no retirement process here, there is a conversation and an afternoon. Building a staged deprecation programme at this size is the same category error as running Kubernetes for three services.
- You cannot fund the tail. Then do not announce. Stop new creation, stop investing, keep patching, and revisit next quarter with a real number and a real owner. Nobody outside your team will notice that you did not announce. Everybody will notice the deprecation that ran out of energy.
One guardrail for the other direction, because the previous bullet is easy to abuse: a road you keep deciding not to retire is a road you have decided to fund forever. That is a legitimate choice, but make it explicitly, with the maintenance in someone’s plan. The failure I see most often is not a bad retirement decision, it is the absence of one, maintained indefinitely because nobody’s number ever changed enough to force it.
The bottom line
Paving roads is the visible half of platform engineering, and closing them is the half that decides whether your estate is a portfolio or an archaeological site. A road that gets deprecated but never retired is the worst artefact a platform team can own: it costs you maintenance, it splits your defaults, it rots while still carrying production traffic, and it teaches the whole organisation that your dates are negotiable.
So treat the retirement like the launch it is. Prove the destination by moving a real service yourself. Ship the migration as a diff. Count the estate and delete everything in it that turns out to be dead. Fund the tail three ways, per team, before you say a word publicly. Block new creation on day one, warn people in their build logs, brown it out on a Tuesday, and then turn it off when you said you would. Delete the template, delete the infrastructure, and write down what it cost.
And before you send the announcement, answer the question the whole thing turns on: who, by name, is going to migrate the last service, and whose quarter does that come out of? If you cannot answer that, you are not deprecating a golden path. You are hoping one goes away, and paved roads never do.
If you are staring at an old paved road you cannot afford to keep and cannot seem to close - or a deprecation that stalled at eighty percent eighteen months ago and is now quietly two platforms - that is exactly the kind of problem I help teams work through. Let’s talk.