What is technical debt? A practical guide with examples
Technical debt is the future work your software owes. A practical guide to what it means, the main types, real examples, and how to measure what it costs.

Tiago Coelho
CTO


Downtime isn't the main risk in a legacy migration. The risk is the period where the old and new systems are both live and nobody can prove they agree.
TSB's April 2018 migration moved the data successfully and still locked out a significant proportion of 5.2 million customers, taking until December 2018 to return to normal and costing £48.65m in fines plus £32.7m in redress.
The safe sequence is Stripe's four phases: dual writes, move the reads, move the writes, then delete the old path. Every phase is individually reversible.
Reconciliation is the deliverable, not the migration. Run both systems on real traffic, compare the answers, and only cut over when the mismatch rate is explained rather than small.
AI closed the gap on the mechanical work, not the verification. Google reported 80% of code changes AI-authored in one internal migration and effort down an estimated 50%, and the review still belonged to engineers.
Migrating a legacy system without stopping the business means never reaching a moment where everything changes at once. You stand the new system up beside the old one, move traffic across in slices you can reverse, and prove the two agree before either becomes the only source of truth. The cutover stops being an event and becomes a series of small, boring decisions.
That's the part most plans get wrong. They treat the migration as a date to hit, then spend the last month negotiating a maintenance window. The window was never the hard part.
Migrating a legacy system without business interruption involves four things: a new implementation running alongside the old one, a way to keep both stores of data current, a way to compare their answers on real traffic, and a rollback that works at every step. Get those four and the migration date stops mattering.
None of that is new. Martin Fowler described the pattern in 2004, as the strangler application, renaming it the strangler fig in 2019. Both AWS and Microsoft publish it as a cloud design pattern, and every large engineering organization runs some version of it. What's changed is that the tooling to do it well, from change data capture to feature flags and traffic shadowing, is now off-the-shelf rather than something you build first.
So when a migration does stop the business, the cause is almost never that the technique was unavailable. It's that the program was scheduled as a replacement rather than a transition.
Legacy migrations stop the business when the plan has a single irreversible step in it. Everything up to that step is theory, everything after it is production, and the organization finds out which of its assumptions were wrong with customers watching. Data migration is rarely the failure. Behavior is.
TSB is the case worth reading, because the failure wasn't where people assume. In April 2018 the bank moved corporate and customer services to a new platform. According to the FCA's 2022 final notice, the data migration itself succeeded, and the new platform "immediately experienced technical failures". Every branch was affected, along with a significant proportion of 5.2 million customers. Business didn't return to normal until December. The bill was £48.65m in regulatory fines and £32.7m in customer redress, and the FCA's Mark Steward put the cause plainly: "The firm failed to plan for the IT migration properly, the governance of the project was insufficiently robust and the firm failed to take reasonable care to organise and control its affairs responsibly and effectively."
The data arrived. The system didn't behave. That distinction is the whole subject of this article, and it's why a migration plan that ends in a weekend is a plan with one test in it.
There's a quieter version of the same failure that never makes the news. The rewrite that runs for two years while the old system stays frozen, because every change has to be made twice. Joel Spolsky called this out in 2000 after watching Netscape rewrite its browser, and the mechanism hasn't changed: the competitor doesn't have to be faster than your new system, only faster than your migration.
A cutover moves everything at a chosen moment. A parallel run keeps both systems live and moves traffic gradually, with the old system available until the new one has nothing left to serve. Both are legitimate, and the difference in what they cost you when something is wrong is large enough to decide most programs.
Cutover | Parallel run | |
|---|---|---|
How traffic moves | All at once, at a planned time | In slices, one route or cohort at a time |
What you learn before committing | What tests and staging told you | How the new system behaves on real traffic |
Cost of being wrong | An incident, plus a restore | One slice reverted, usually unnoticed |
Rollback | A single plan, rehearsed at best once | Exercised on every slice |
Data | Moved in a window, verified after | Kept current in both, compared continuously |
Elapsed time | Shorter on paper | Longer, and it overlaps normal delivery |
Best when | The system is small, or a hard external date sets the schedule | The system carries revenue, compliance obligations, or customers who'd notice |
Worst when | The system is business-critical and its behavior isn't fully documented | Two-store consistency genuinely can't be maintained, which is rarer than teams assume |
The honest case for a cutover is a small system, or a deadline you don't control: a vendor withdrawing support, a data center closing, an acquisition that has to complete. Even then, the useful question is which slices could move earlier rather than whether the window can be shortened.
How much of each service you change on the way across is a separate decision from how it moves, and the three answers (rehosting, replatforming or refactoring) carry very different amounts of risk. Settle that first, per service. Then plan the crossing.
You move data without a freeze window by writing to both stores for a period, backfilling the history behind you, then switching reads and writes independently. Stripe published the four-phase version of this and it remains the clearest description available: dual writing, changing all read paths, changing all write paths, and removing the old data.
The order carries the safety. Dual writing means the new store is current from day one, so the backfill only has to catch up on history rather than race live traffic. Moving reads before writes means the new store gets exercised on production queries while the old one is still authoritative, so a wrong answer is a discrepancy in a report rather than a corrupted record. Moving writes after that is the first genuinely committing step, and by then you've been reading from the new store for weeks. Deleting the old table comes last, long after everyone has stopped being nervous.
Stripe's own illustration of why this needs machinery rather than a script: migrating 100 million subscription objects at one per second would take over three years. Backfills get parallelized, and they get re-run, which means every step has to be idempotent: safe to run twice without changing the result.
Change data capture is the common alternative to application-level dual writes, streaming the old database's write log into the new store. It's less invasive because the application doesn't need to know, and it moves the reconciliation problem rather than removing it: you now have a replication lag to reason about instead of a write path to maintain twice.
You prove it by running both on the same real traffic and comparing the results continuously — not by testing the new system in isolation and inferring equivalence. Send each production request to the old path and the new one, return the old path's answer, log the difference. Do it for long enough to see a month-end, a payroll run, a Black Friday.
This is the discipline the industry named and then mostly forgot. GitHub open-sourced Scientist for exactly this: run the legacy code path and the candidate side by side, return the legacy result, and record where they diverge. Stripe used it while moving read paths. The pattern has ports in most languages, and the value isn't the library. It's that comparison becomes a build artifact instead of an opinion.
Two things matter more than the tooling. The first is that a mismatch rate near zero isn't the goal: an explained mismatch rate is. A migration where 0.3% of comparisons differ and every category is understood (rounding, time zones, a legacy bug the new system declines to reproduce) is safer than one at 0.01% where nobody can account for the remainder. The second is that someone has to own reconciliation as a named responsibility, with a number they report weekly. When it's everybody's job it becomes a dashboard nobody opens.
Verification is also where the constraint usually bites. If the critical paths have thin test coverage, building that coverage is the first project rather than an overhead on the second. Test debt is what makes every other kind of debt too risky to repay, which is why we spend a lot of the early weeks of a legacy modernization engagement there.
The first slice should be the one with the smallest blast radius that still exercises the full path: request in, data written, response out, monitoring showing it. Not the most valuable service, and not the least. Something real enough to teach you where the plan is wrong, small enough that being wrong costs a revert.
Read-only endpoints are the usual answer, because comparison is trivial and mistakes don't persist. Internal admin tools are the second, since the users can tell you what looked odd. What to avoid first is the thing everyone proposes: the hardest, highest-value service, on the theory that if it works everything else will. It inverts the point of slicing. You want the cheap lessons before the expensive commitment.
The sequence after that follows dependencies rather than value. Anything that writes to a table you haven't migrated stays put until you have. Once two or three slices are through, the team's estimates on the rest become worth something, which is a better basis for a roadmap than the estimate you'd have written at the start. This is where a proper application modernization strategy earns its keep: it sequences the whole estate, while this article covers how any one piece of it moves.
These are conditions, not phases, and traffic shouldn't move until each one has a name against it:
Test coverage on the paths you're moving. Not overall coverage. The specific behaviors this slice is responsible for.
A rollback you've executed. A documented rollback that has never been run is a hypothesis.
Feature flags at the routing layer, so reverting a slice is a config change rather than a deploy.
Reconciliation reporting, with an owner and a weekly number.
Observability that spans both systems, so you can compare latency and error rates rather than argue about them.
Idempotent backfills, safe to re-run without double-counting.
A named decision-maker for each cutover, with the authority to stop it on the day.
Support and finance in the loop, because they'll see the discrepancies before engineering does.
That last one gets skipped most. The people who notice a migration going wrong first are almost never on the migration team. They're the support agent seeing two versions of an account and the finance analyst whose reconciliation won't close. Giving them a channel that reaches the engineers directly, and a heads-up about what to expect, is worth more than another week of staging tests.
AI has compressed the mechanical work of migration substantially, and left the verification work almost exactly where it was. That's a real change and a narrower one than most vendor claims suggest, and it doesn't shift the risk profile of a cutover at all.
The best-documented figures are Google's. In its account of using large language models for internal code migrations, 80% of the code changes in the landed change lists for a 32-bit to 64-bit integer migration were AI-authored, the overall migration effort came down by an estimated 50% as reported by the engineers doing it, and the model predicted which Java files needed editing with 91% accuracy. Across migrations, more than 75% of AI-generated character changes landed. Note what those numbers describe: repetitive, well-specified transformations across thousands of files, with human review on every change list, inside a monorepo with unusually good tests.
Which is the useful lesson for a legacy migration rather than a discouraging one. The work AI absorbs is the work that scales with file count: translating a framework idiom, updating call sites, generating the tests you should have had. The work it doesn't absorb is deciding what the system is supposed to do, and proving the new one does it. Those were always the expensive parts. We're an AI-native Digital Product Studio and we put agents on the mechanical layer routinely. The reconciliation number is still read by a person who's accountable for it.
Reversibility looks like caution and behaves like speed. A migration built from steps you can undo lets you move before you're certain, which is the only way anyone ever gets moving on a system nobody fully understands. A migration built around one irreversible step forces you to be certain first, and certainty about a twelve-year-old codebase takes longer to manufacture than the migration does.
The systems worth migrating are the ones carrying the business, which means they were never going to hold still while you replaced them. The interesting question isn't how to stop them for a weekend. It's how to change them while they're running, one slice at a time, with the option of putting the slice back. That's a question about test coverage, reconciliation and ownership far more than it's a question about architecture.
Legacy system migration is moving an older application, its data, or both onto new infrastructure, a new platform or a new architecture, while preserving the behavior the business depends on. It differs from a rewrite in that the existing system's behavior is the specification. Most migrations run incrementally, with the old and new systems live at the same time and traffic moved across in stages.
Usually yes, for the customer-facing path. Running the new implementation alongside the old one, keeping both data stores current with dual writes or change data capture, and moving traffic in reversible slices avoids a maintenance window entirely. What's harder to avoid is a short freeze on a single write path at the moment writes switch over, typically measured in seconds to minutes rather than hours.
Timelines vary too much by system to promise a number, but the shape is consistent: the first slice takes disproportionately long because it builds the machinery, and the rest goes faster once dual writes, reconciliation and rollback exist. Programs that quote a single date for a whole estate are usually quoting the cutover rather than the migration.
The strangler fig pattern, which Martin Fowler described in 2004 and renamed from strangler application in 2019, means building the new system around the edges of the old one and routing functionality across piece by piece until the old system has nothing left to serve. A routing layer sits in front of both and decides which handles each request. Both AWS and Microsoft publish it as reference guidance for incremental migration.
A dual write means the application writes each change to both the old and the new data store, so both stay current during a migration. What goes wrong is partial failure: one write succeeds and the other doesn't, leaving the stores disagreeing. That's why dual writes need idempotent retries and a reconciliation job that finds and repairs drift, rather than being trusted on their own.
Migrate incrementally in nearly all cases where the system is in production and carrying revenue. A full rewrite requires freezing the old system's development or maintaining every change twice, and it defers all value to a single delivery date. Rewrites make sense mainly where the existing behavior is genuinely not worth preserving, which is a much smaller set of systems than it feels like from inside one.
One named person per cutover, with the authority to stop it on the day and no incentive to see it through. In practice that's usually the engineering lead for the slice rather than the program sponsor, because the sponsor is measured on the date. Write the name down before the week of the cutover, along with the specific signal that triggers a revert.
When the old system has served no traffic for a defined period, reconciliation has been clean or fully explained across at least one full business cycle including a month-end, and someone has confirmed nothing reads from it out of band — reporting jobs, exports and integrations are the usual surprises. Then remove the old path in a separate, deliberate change, not as part of the cutover.
Not usually the whole system, but the data layer is often the gate. Retrieval and agent features depend on data being consistent and accessible, and in older systems it typically isn't. That makes restructuring the data a narrower and earlier project than migrating the application, and it's frequently the piece worth moving first.

Tiago Coelho
CTO
As CTO of Pixelmatters, Tiago Coelho leads the engineering organization, wrote the handbook and quality standards it ships against, and is rebuilding delivery around AI. He writes on technology strategy and engineering leadership.
Share this article