Out of Support, Out of Options

Out of Support, Out of Options

Every business is right to be afraid of a rewrite, and refusing one is usually the correct answer. But every year the upgrade is deferred, the payment gets larger rather than later, and a codebase that never pays anything down eventually reaches a version it cannot leave. Then the argument is over. What the deferral actually costs, and how I would run the rewrite nobody wanted and nobody can avoid.

The Answer Is No, And It Is Usually Right

Joel Spolsky's argument from April 2000 still stands: rewriting from scratch is the worst strategic mistake a software company can make. Netscape rewrote its rendering engine, went roughly three years without a competitive release, and handed the browser market to somebody else. The reasoning underneath is the part that lasts. Old code looks bad because it is full of fixes, and every branch that makes no sense is a bug report somebody resolved at two in the morning. Throw the code away and you throw away the bug reports, and the new system rediscovers all of them in production.

Then the business objection. A rewrite is a very large amount of money spent so that, on the day it ships, the product does what it already did. No feature to announce, no new customer, and the best available outcome is that nobody notices. None of which is an objection to the engineering. The usual failure is that the rewrite is late, and while it is late the old product still needs changing, so one team maintains two systems against a target that moves every month. Most rewrites die of that, quietly.

Then You Hit The Wall

An application whose oldest running code was written around 2006. A desktop UI toolkit, a runtime version chosen because that is where it landed the last time anyone had budget, and a dependency list nobody has moved in a decade. Then the requirement arrives: it has to run somewhere other than the operating system it was written for. A significant customer's estate is Linux, or the deployment story has to be a container, or sales has already promised it. Platform support has become a product requirement instead of an engineering preference, which is the only kind that settles anything.

Now walk the upgrade path and watch each door close. The runtime is years past end of support, and moving to the last release in that family buys back supportability while contributing nothing, because the whole family is tied to one operating system. The UI toolkit looks like better news, since it was carried onto the modern cross-platform runtime, right up until you read the note explaining that it stays single-platform by design, because it draws through that platform's own windowing APIs and always has. A meaningful share of the dependencies have no release targeting where you need to go. And the primitives the application is built out of, the way it serializes state and talks between processes, were dropped on the way to the modern runtime with no deprecation period at all.

Anyone who has done this on the Microsoft stack will recognize the specifics, and the specifics are not the point: the Python 2 codebase whose native extension has no maintainer, the Ruby so old that half its gems no longer build, the Java service whose reflective machinery the module system stopped tolerating.

That structure is the wall. No part of it is a single blocker you can cost out and schedule. The runtime, the toolkit, the dependencies, and the process model each fail the requirement independently, and not one of them fails in a way a package upgrade addresses. Replacing the UI does not help while the process model is unportable; replacing the process model does not help while the UI is single-platform. No order leaves a shippable product after each step, which is precisely what incremental modernization cannot fix. What is left is a rewrite, one operating system forever, or telling the customer no.

The Interest Nobody Books

Ward Cunningham coined the debt metaphor in 1992 and spent years correcting how people used it. The misreading he kept correcting was that debt means bad code. In his account debt is the distance between what the code says and what you have since understood about the problem, and that you pay interest on that distance in every change until you close it. So you can write immaculate code against a 2006 understanding of the world and owe just as much.

Now take the decision as it is actually presented. On one side a runtime upgrade with no customer-visible outcome, regression risk across the whole product, and an estimate nobody trusts. On the other, a feature with a named customer waiting. Defer the upgrade. Deferral is the correct call on the information available, and I would make it too. Then make it again next quarter, because the reasons do not change. Nineteen consecutive correct decisions produce an outcome nobody would have chosen and no single person can be pointed at.

Why teams defer is the easy half. The puzzle is why deferral costs so much more than it looks like at the time. Four mechanisms, none visible in the quarter you skip:

  • Skipped upgrades multiply, they do not add. One at a time, each is a small set of breaking changes against a codebase you understand, with a working system either side to compare against. Eight at once is every breaking change simultaneously and in combination, with no intermediate working state and no way to attribute a failure to its cause. The combined upgrade is a different activity altogether, and one that resists estimation.
  • The knowledge decays on the same clock as the code. The person who could have done the upgrade in six weeks in 2015, because they wrote half of it and remembered why, retired in 2021. The cost went up while the number of people able to pay it went down, and the same passage of time drives both.
  • The window is not yours to choose. Support windows, vendor lifetimes, and platform requirements run on somebody else's calendar. A team planning to pay eventually is assuming the option stays open, and the date it closes is set by people who have never heard of your roadmap.
  • You lose the ability to tell "cannot" from "have not tried". After enough years the answer to a new request stops being an estimate and becomes a reflex. Whether the thing is possible goes unchecked. Everyone already knows what the answer will be. The debt has stopped being a property of the software and become a property of the company.

So the payment does not sit still waiting for a better quarter. It grows faster than the codebase does, and at some point crosses from a number you could have paid into a number nobody will authorize. The wall above is what the interest eventually buys.

Timely paydown cannot be a project. A paydown project competes with features for funding and loses every time, and deserves to, because features have customers attached. What works is a standing allocation spent before the argument happens: a fixed share of every cycle, dependencies moved continuously instead of in batches, one support release at a time with no skipping. None of that needs a business case, which is what lets it survive. Test coverage answers a different question. The best single number for how much trouble a system is in is how many support windows it is currently outside of, and for how long.

What It Is Already Costing You

The largest cost is the thing you never got to do. The one a board understands and the one engineers mention last. The maintenance tax is real and it is not the point. The point is optionality: when the market moves, or the biggest customer asks for a capability, the answer is no. Not "that will take a while": no, because the change would go through a part of the system nobody will touch. It shows up as a deal that quietly went elsewhere, and as a strategy meeting where an obvious option was never raised because everyone had learned not to raise it. You cannot put a figure on the roadmap you stopped imagining, which is why it never appears against the upgrade deferred to protect a roadmap.

Every change is priced for the worst change. With no test coverage and no seams, the cost of a small change is the cost of establishing that nothing else broke, and that is roughly constant however small the change was. Teams start quoting six weeks for two days of work, and the estimate is accurate: almost all of it is verification. No velocity chart shows it, because the work that would have shown it never gets proposed. The symptom is that nobody suggests small improvements any more, and nobody notices that nobody does.

The security exposure is not mainly a list of CVEs. An unsupported runtime gets no fixes, which is the easy part to explain. The commoner exposure is duller, because old runtimes make their own security decisions instead of deferring to the operating system. Transport security is the classic: the application negotiates with the protocol and cipher list current when its runtime shipped, however thoroughly the platform underneath has been hardened since. Worse is the build, which nobody counts as exposure at all. When something urgent lands in a component you ship, upstream having a patch settles nothing. The question is whether you can still produce a signed, tested release this month.

It quietly selects your team for you. The engineers content to work on it are largely those who already know it. They are closer to retirement every year. Everybody else takes the other offer, because two years on a stack with no future is two years of career they do not get back. So the codebase that most needs strong engineers is least able to attract them, and the knowledge concentrates in fewer heads in the same direction as the risk.

Expensive Or Impossible

Name the requirement, and make it the customer's. The rewrite has to be justified by something the business already agreed to sell or is already obliged to do. Cross-platform because a signed contract requires it. An audit finding no unsupported runtime can clear. If the requirement reduces to "we would like a modern stack," there is no case here, only a preference, and it should lose.

Cost the ugly incremental options first. Price the alternatives as though you wanted one to win. Run the old application in a VM and stream it. Ship it in a container for the platform it does run on. Put a thin web front end on the same backend for the environments that need one. Every one is worse architecture and at least one is frequently good enough for another five years. An ugly thing that ships in a quarter beats an elegant thing that ships in two years, and the two-year plan is the one cancelled in month fifteen.

Show that each blocker is structural. A dependency you cannot upgrade because it would take four months is a budget problem wearing a technical costume. A dependency with no version that runs on the target at all is a different category. Write every blocker down and mark which it is. If they all land in the first, you are asking for a rewrite to avoid tedious work.

Say what will not improve, and price doing nothing. The new system will not be faster, will not have the features people asked for, and will introduce bugs the old one fixed a decade ago. If you cannot say that in the room where the money is, you are selling rather than proposing. Then price the alternative. The deals that cannot be bid, the audit finding that returns, the engineers who will leave. That answer has to be allowed to come back small, because if doing nothing genuinely costs little this year then it can wait.

Step One: What Does It Do?

After twenty years, nobody knows. Not the soft version of that claim, where it means nobody wrote it down. It is genuinely unknown. The people who wrote the original have left, and the behavior the product is genuinely valued for may be a side effect nobody designed. A menu item three customers use, and one of them is your largest. An export format built for a partner integration in 2011 that some regulator now expects to see. And there is a module everybody is confident is dead which runs every night at 2am and has never once failed, which is exactly why nobody has thought about it.

So nothing gets designed yet. The first deliverable is an inventory of what the product does, with evidence attached to every line. Four sources, none sufficient alone:

  • Instrument the old product. The highest-return week in the whole program, and it happens before anything is designed. Add usage telemetry, ship it, and let it run a full business cycle: a quarter minimum, a year if the customers are seasonal, because the feature used once at year end is the one you would otherwise miss. Count distinct customers per feature, not invocations, or one automated user makes a dead screen look busy.
  • Read the support tickets and the training material. Every workaround support routinely gives customers is an undocumented requirement, and so is every "you have to do these in this order" the trainers teach. Support has been maintaining the real specification for years without anyone asking them to.
  • Go through the contracts and the regulated behavior. Some features exist because a customer's contract names them, or an auditor accepted them once. They generate almost no volume, so telemetry ranks them near zero, and they are not negotiable.
  • Read the code last, and only where the first three pointed. Archaeology across a whole system is an effective way to spend a year producing a document nobody reads.

Two things to hold onto. Chesterton's fence: when you find code whose purpose nobody can explain, the position to hold is that you do not yet know whether it matters, not that it does not. In old software this is the workaround with no comment, and that is nearly always load-bearing, because something broke once, in production, and this stopped it. And Hyrum's law, which holds that with enough users every observable behavior gets depended on by somebody, specified or not. In a twenty-year-old product that is the main finding: the exact column order in an export, the direction a rounding error has always gone, a temporary file left where a customer's script has read it since 2014. None of it is in any specification, and each generates an escalation the week you change it.

Step Two: What Is Not Coming

Sort everything into four buckets, and get the result signed:

  • Rebuild as is. Used, understood, depended on. Port the behavior faithfully, including the parts you are certain are wrong.
  • Rebuild differently. The need is real, the implementation was a product of its decade. Rebuilding those is where the value of a rewrite lives, and it should be a short list, because every entry is a retraining cost imposed on people who never asked for a new product.
  • Not coming, with a migration path. Low usage, but somebody real is using it. A documented alternative, a date, and a named person whose job is to tell those customers before they find out by discovering it missing.
  • Not coming. Zero measured use across a full cycle, no contract, no regulator. Deleted, with the reversal cost written beside it, because some cost a week to add back and some are architectural.

The signature matters more than the buckets. This list gets signed by whoever owns the revenue, not by engineering, for an entirely practical reason. In month fourteen a customer will escalate about something in the fourth bucket, and the only question anyone asks is who decided. If engineering decided, it is reversed within a day, the scope comes back, and every subsequent cut becomes negotiable, which means there are no further cuts. If a commercial owner agreed in writing with the usage data in front of them, it holds, and it holds for the other items too.

Step Three: Where To Cut

The pattern is Martin Fowler's strangler application, described in 2004 and named for the fig that grows around a host tree until it can stand without it. The new system runs alongside the old, capabilities move across one at a time, both live throughout, until nothing is left in the middle worth removing. The strangler is the only shape I have seen work at this size. The reason is not technical. It produces something to show every few weeks, which is what keeps a program alive across a change of sponsor.

Concretely:

  • Keep the database. The largest de-risking decision available, resisted for a bad reason: the schema is bad. It is also the one artifact whose meaning survived twenty years intact, and rewriting client and storage at once means the two systems can never run side by side, which throws away the whole strategy. Migrate the schema afterward, once only one system is talking to it.
  • Get the logic behind an API before writing any new UI. The first real task is pulling business rules out of the event handlers behind the screens, which in a desktop application of that age is exactly where they live, and putting them behind a service boundary the existing client calls. Everything incremental depends on this step, and on its own it improves a product you have not yet committed to replacing, which makes it the safest money in the plan.
  • Put an anti-corruption layer between old and new. Translate explicitly at the boundary, or the new system quietly takes the shape of the old one and you have paid for a rewrite and received a reimplementation.
  • Write characterization tests, not correctness tests. Michael Feathers made the point in 2004: with legacy code you are not asserting what the system should do, you are pinning down what it currently does, wrong parts included. Record real inputs from production alongside their outputs and make the new implementation reproduce them. Where it deliberately differs, that belongs in the rebuild-differently bucket with a signature on it.

The ordering this produces is the reverse of what everybody wants to do. Logic behind an API first, with the old UI still on top and nothing visibly different to anyone. Then a new UI against that API, for one capability. Then capability by capability. The visible part comes last within every slice.

Step Four: Picking The Target

The technology choice is the part everyone wants to start with and the part that matters least, provided it is made against the requirement that forced the rewrite rather than against taste.

Does it target everything in the requirement? Check that first and let it end the shortlist, because it routinely gets checked last, after a comparison table has already made somebody's favorite look good. The first-party option is the comfortable default and is frequently the one that does not cover the platform you were forced onto: vendors build cross-platform tooling for the platforms they care about selling into, and that list is not your list. The question worth asking before any of it is whether the answer is a desktop application at all, because if the logic is going behind an API anyway, a browser client is cross-platform by construction. A browser is worse for offline operation, hardware access, and the dense keyboard-driven screens that professional users get fast on, and any of those may be decisive. Answer it on evidence, and do not skip it because the old application happened to have windows.

Step Five: Shipping It In Pieces

The delivery model is what separates a program that survives a change of sponsor from one that does not. Easy to state and hard to hold: the new system gets in front of real users early, in pieces, doing something they can see.

Slice by what a user does, and pick the first for evidence. The tempting plan is to build the data layer, then the services, then the UI. It parallelizes cleanly and delivers nothing until the very end, which turns the whole program into one bet settled on one date. Slice vertically instead: one complete capability, storage to screen, that a real person uses to do a real part of their job. Choose the first to be small, used often enough to give feedback within weeks, and not the one that ends the company if it misbehaves, because its job is to prove the seams, the deployment, and the accuracy of your estimates.

Both products run at once, and that is the design. Users will have the old client and the new one open at the same time, for a long period, quite possibly years. Design for it deliberately: one login, shared data, and a published statement of which system does what this quarter. The instinct is to treat it as an embarrassing transitional phase and say as little as possible, and that produces the worst version, where people meet the boundary by surprise instead of being told where it is.

Measure the program in users moved. Percent-complete on a rewrite is fiction, and a specific kind: the remaining work is by definition the work nobody has examined, so the number is most wrong exactly when it is most reassuring. Track how many real customers are doing how much of their real work in the new system. Users moved is a harder number to make look good, which is the reason to use it.

Keep fixing the old product, under a published policy. The old system does not politely pause while you build, and every change request will arrive sounding reasonable because it will be. One policy works. Security and regulatory fixes go in without argument; anything else only if the affected capability has not yet been rewritten. Publish it at the start, because relitigating it request by request is how a team ends up building two products and finishing neither.

Agree the switch-off condition while everyone is optimistic. Decide in advance what ends the old system: a usage threshold, a date, or both, with a named owner. Decide it in month one, because the last few percent of users will not move voluntarily and by then nobody will want the argument. Without a trigger the old system is still running five years later, which means the rewrite replaced nothing. It added a second product to maintain.

Against Myself

The discovery phase is expensive and produces no software, and "trust me, the inventory pays for itself" sounds exactly like what it sounds like. The best mitigation I have is that two of its outputs, the telemetry and the logic-behind-an-API work, improve the existing product whatever gets decided afterward. Strangling also costs more in total than a clean rewrite would, if the clean rewrite worked: you build adapters whose only purpose is to be deleted later, and run two systems for years while paying for both. Anybody selling incremental delivery as the cheaper option is wrong. Strangling is more likely to finish, which is a different virtue.

And all of it rests on the claim that the wall is real in the case in front of you. I have also sat in rooms where a very similar argument was made, sincerely and fluently, about a system with six good years left in it, by people whose actual position was that they did not want to work on it any more. I do not exempt myself from that, which is why the test comes before the plan rather than after it.

What I Would Do

  • Before any of it: count how many support windows the system is currently outside of, and fund a standing allocation instead of a paydown project, because a paydown project competes with features and loses. A standing allocation is the only item here that prevents the other nine.
  • Make the case on a requirement the business already owns, never on the quality of the code. Price the ugly incremental alternatives first, and take one if it works.
  • Instrument the old product before designing anything, and let it run a full business cycle. Distinct customers per feature, not invocation counts.
  • Build the inventory from telemetry, support tickets, and contracts, and read the code last.
  • Sort it into rebuild, rebuild differently, migrate off, and delete. Get it signed by whoever owns the revenue.
  • Keep the database. Pull the logic behind an API the old client calls, before a line of new UI exists.
  • Pin current behavior with characterization tests recorded from production, including the behavior you are certain is wrong.
  • Choose the target against the constraint that forced the rewrite, and write down what would change your mind.
  • Deliver vertical slices. The first chosen to prove the machinery, the second chosen for value.
  • Report progress as customers doing real work in the new system, and refuse to report a percentage.
  • Publish the old product's freeze policy on day one, along with the condition under which it gets switched off.

The ordering is the argument. Almost everyone treats a rewrite as a technology decision and opens with the framework comparison, which is the one choice that can be reversed later and the one that absorbs all the attention. The decisions that cannot be reversed are what you keep, what you drop, and who put their name to it.

And for anyone not yet in this position, which is most people reading it: none of the above is the expensive part. The expensive part already happened, one entirely defensible quarter at a time, years before anybody said the word rewrite out loud. If your product still has a live upgrade path, all of this is avoidable for a fraction of a cycle a year spent on work no customer will ever thank you for. That is a terrible pitch and it is the conclusion.