İçeriğe geç
wedevit

August 3, 2026 · 9 min read · software

İlhan Buğra Aslan

Rewrite the legacy system, or replace it slice by slice?


Rewriting an old system from scratch is usually the most expensive and riskiest option on the table. The safer route is to replace it in slices: put a routing layer in front of the existing system, build one capability on the new side, send only that share of traffic to it, validate in production, then switch the old code path off. Martin Fowler named this approach after the strangler fig tree, whose seed germinates in the branches of a host tree, sends roots down, and over time wraps the host and takes its place. A full rewrite is sometimes the right call, but only under specific conditions, and we list those conditions further down.

Why the rewrite decision looks so attractive

The idea usually arrives as a technical complaint. Nobody can read the code, nobody dares touch it, a two-line change takes two weeks. Joel Spolsky's April 2000 essay on the subject still circulates because it explains why that complaint misleads. Reading code is harder than writing it. Those odd, apparently pointless lines inside a function are often bug fixes hunted down one at a time over years; each one cost somebody days of debugging, and none of it is written down anywhere. Throw the code away and you throw that knowledge away with it, then get to rediscover the same bugs. Spolsky's example was Netscape, whose decision to rebuild the browser for 6.0 opened a gap of nearly three years between major releases, and the market share went away in that gap.

The second problem is the calendar. Fowler's observation is blunt: full rewrites go down in flames most of the time, because they take years and the business keeps asking for new features throughout those years. That leaves two options. Freeze the feature set, in which case competitors walk past you and the project collects internal enemies. Or build every new feature twice, in which case the cost doubles and the finish line keeps moving. This is where the incremental route actually pays: both the investment and the return arrive in small, visible increments.

How slice-by-slice replacement works

Microsoft's architecture guidance describes the pattern in four phases. First, a façade sits between the client and the legacy system, initially routing nearly everything to the legacy side. Second, as each capability becomes ready on the new side, the façade changes where those requests go, and the legacy system's remit narrows with every round. Third, once nothing depends on the legacy system any more, it gets decommissioned. Fourth, the façade comes out and the client talks to the new system directly. Users notice none of this, because the address and the interface stay the same throughout.

Choosing the first slice decides how the project goes

The common mistake is to start with the part that annoys everyone most, which is usually the most tangled core of the system. That choice pushes the first real result out by months, and the budget gets questioned while the project has yet to deliver anything. The criterion Thoughtworks recommends is more useful: pick something heavily used but not among the most complex pieces. Heavy use means the new code gets tested by real traffic and real edge cases. Moderate complexity means the first slice ships within weeks, which buys credibility with the team and with management at the same time. Coupon lookups, address validation, PDF generation, notification delivery: endpoints that sit at the edge but get called constantly make good first candidates.

With no spec, the existing system is the spec

What legacy systems have in common is that nothing about their behaviour is written down. So measure the behaviour before writing any replacement code. Three practical moves: write automated functional tests that treat the system as a black box, record current response times and load profile, and collect logs from an intercepting layer so you can see real usage without editing legacy code. Do not skip the performance baseline. A new version that works but runs three times slower does not work as far as the business is concerned.

Most teams hit a surprise here. An integration nobody knew about, a forgotten endpoint still called a few times a day, or a module unused for years that everyone is afraid to delete. Do not migrate unused modules, delete them. Code you never move is the cheapest code you will ever ship.

The hard part is the data, not the code

Legacy systems typically lean on one central database serving many business domains at once, and that is where the real cost of the migration sits. Microsoft's guidance suggests a three-step sequence on the data side. First the new service starts handling requests for its own domain while still reading and writing against the legacy database. Then the tables and stored procedures for that domain are extracted into a new database: history moves across with an ETL job, and the delta stays in sync through change data capture (CDC). Two databases run side by side at this stage, and the real work is not writing code but proving the two agree. The last step is the cutover: the new database becomes the system of record for that domain, the old tables come out, and the next domain begins.

That ordering has one property worth planning around. The rollback window stays open until the old tables are actually dropped, so if something goes wrong you can send traffic back, because the data still exists in both places. Once the tables and the sync jobs are gone, rolling back means restoring the dropped objects and replaying every change since, and the cost jumps. Treat dropping legacy objects as a deliberate, separate final step per domain, not as part of cutover night.

Keep the new system out of the old system's semantics

While the migration runs, the two systems have to call each other. The new side needs a function that has not moved yet, and the legacy side needs one that already has. Wire those calls together directly and the legacy data model, field names and strange status codes leak straight into the new system. The fix is a translation layer in between, known in the pattern literature as an anti-corruption layer. It translates between the two vocabularies and insulates the new design from old habits. It is very easy to skip, and the price is steep: two years later you own a newer copy of what you set out to replace.

Transitional architecture costs money, so put it in the budget

The façade, the translation layer, dual writes, sync jobs, two environments running at once. None of this survives into the finished product; it is all temporary, written to be thrown away. The pattern literature calls it transitional architecture, and the question to ask is whether the cost of that temporary scaffolding is worth the risk it removes. For most enterprise systems it is. But it belongs in the budget explicitly, or the "why are we paying for two systems" question in month six stops the project halfway through.

Two technical cautions about the façade. It must not become a single point of failure, since all traffic now passes through it. And it must not become a bottleneck, so measure the latency it adds from day one. It is also a good place to consolidate authentication and authorization, though do not copy the legacy session logic across as-is. A migration is one of the rare openings to clear authentication debt that has been deferred for years, and the same goes for secrets buried in the code.

When a full rewrite is the right answer

The incremental route does not fit every system. Microsoft's guidance is explicit about when the pattern does not apply: when requests to the back end cannot be intercepted, when you have no access to the legacy source code (you need to be able to edit it to switch off migrated features and redirect internal calls), when the system is small enough that replacing all of it is simple, or when the old solution has to be shut down completely and quickly. Add one more: if the platform underneath has reached end of support, you no longer set the schedule, and the risk of unsupported software sets it for you. Even then, buying time with a wrapper first and slicing afterwards tends to beat a single-step cutover.

The most expensive outcome is a migration that stalls

Slicing has its own characteristic failure mode: the project stops halfway and leaves you with two systems, two teams, two deployment pipelines and a routing table nobody fully understands. The way to prevent it is to measure progress with numbers rather than intentions. Two metrics are enough: the percentage of requests the legacy system still serves, and how much code is left in it. If both are not falling every month, the migration has stopped. Write the shutdown date for each slice up front, too, because a slice is not finished until the code it replaced is deleted.

Technology is at most half the problem

Thoughtworks repeats a line throughout its series on legacy displacement patterns: technology is at most half of the legacy problem, and ways of working, organizational structure and leadership matter just as much. In practice that means if you leave the team structure that produced the old system untouched while building the new one, you will reproduce the same boundaries in new code. Conway's law spares nobody. Who owns the new system, which team owns which slice, and who makes the architecture calls? Those three answers should come before the technology choices.

A first step that fits in this week

Before deciding anything, produce three lists. One: every entry point into the current system (HTTP endpoints, scheduled jobs, file transfers, other systems connecting straight to the database). Two: the call count for each of them over the last 30 days. Three: how many tables each one touches. Put those three columns side by side and the first slice usually names itself, being something called often that touches few tables. For designing the interfaces on the new side, see our piece on the API-first approach, and for when buying a product beats modernizing at all, see off-the-shelf or custom software.


Need help with this topic?

get in touchall posts