Legacy Application Modernization Strategy: Strangler Fig or Rewrite
For most CTOs, the safer legacy application modernization strategy is the strangler fig. You put a routing layer in front of the old system and replace it one slice at a time. A full rewrite only wins when the old system is small, well understood, or so broken that nothing in it is worth keeping.
You probably have a system that still earns money but fights every change. Releases are slow, and only one or two people understand the core. Now the board is asking whether to "just rebuild it". This article covers how that choice plays out in the architecture itself.
How does each legacy application modernization strategy actually work?
A full rewrite builds a new system next to the old one and switches over in a single cutover. A strangler fig sends traffic through a seam, moves one capability at a time to new code, and retires old parts as they stop receiving traffic.
On paper, the rewrite looks cleaner. You design from scratch, drop the old compromises and launch once. The catch is that the old system keeps changing while you build. Every bug fix and new business rule added to production during the rewrite becomes a gap the new system must close before cutover. That gap often grows faster than the team can close it.
The strangler fig swaps one big risk for many small ones. Each slice goes live by itself and real users test it. If a slice fails, you roll back one route instead of the whole business.
Where should the routing seam go?
Put the seam at the outermost boundary you control. Usually that is an API gateway or reverse proxy in front of your web and mobile clients. It gives you one place to decide, route by route, whether a request goes to old code or new.
The options, in order of preference:
HTTP edge (NGINX, Envoy, AWS API Gateway). Works when clients talk to the system over HTTP. You can route by path, by tenant or by a percentage of users.
Message or event boundary. If the old system already publishes events or reads from a queue, new services can subscribe and take over handlers one at a time.
Inside the code (branch by abstraction). Wrap an internal module behind an interface and swap the implementation. Use this when there is no clean external boundary, such as a desktop app or a nightly batch job.
The decision rule is simple. If you cannot route by URL, route by event. If you cannot route by event, abstract inside the code. Do not pick a seam just because it is easiest for the team if it means you cannot switch users back within minutes.
The first slice matters too. Choose something with clear inputs and outputs and little coupling to the core data model, such as reporting, notifications or a read-only catalogue. Leave billing and the core order flow for later.
How do the old and new systems share data while both run?
While both systems run, they have to agree on what is true. There are only a few honest ways to do that, and each has a cost.
Shared database. The new code reads and writes the old schema. It is the fastest way to start. The risk is that the new code inherits the old data model and you never really leave it behind.
Separate ownership, synced by change data capture. Tools like Debezium stream row changes from the legacy database into the new store. The new service owns its tables, and the old copies stay in step until they are retired.
Dual writes from the application. Easy to describe, hard to get right. If one write fails, the two stores drift apart without anyone noticing. Only use it with a reconciliation job that compares records every day and alerts on any mismatch.
One rule is worth enforcing: at any moment, every table has exactly one written-down system of record. Moving ownership of a table is a release with a sign-off. It should never happen as a side effect of a deploy.
Why do incremental replacements stall halfway?
They stall once the easy slices are done and what remains is a tangled, under-tested core that nobody owns. The team ends up running two systems with no end date, which costs more than either system alone.
Warning signs to watch for:
No new routes have moved behind the router in two or three months.
New features are going into the legacy code "just this once" because it is faster.
Nobody can name the date the old database goes read-only.
New services call back into the legacy system for core business rules, so the old code can never be switched off.
The fix is unglamorous. Track a retirement metric such as routes moved, tables retired or legacy code deleted. Freeze new features in the old system. Give decommissioning its own scope and its own owner.
Running two systems forever is the most expensive outcome on the table.
What does getting this wrong cost you?
A stalled strangler fig leaves you running, securing and staffing two stacks, with engineers switching between both every week. A failed big-bang cutover is worse: data missing or mismatched on launch day, customers locked out, and a rollback plan nobody rehearsed.
In both cases, the business loses a year or more of roadmap and ends up roughly where it started, with less trust in engineering. The architecture decision is really a decision about how much of that risk you take on at once.
When is a full rewrite the right call?
Not every system needs a strangler fig. A rewrite makes sense when the application is small enough to rebuild in a few months, when the business rules are few or well documented, or when the platform is dead (an unsupported runtime, a vendor that no longer exists) and you cannot place a seam.
If your app is an internal tool with a handful of screens and a few dozen users, rebuild it and skip the routing layer. You likely do not need an outside team for that.
What should you require from a modernization team before any code is written?
Ask for documents before demos. A team that cannot produce these up front is guessing.
A capability map with a proposed slice order. Each position needs a reason. This shows whether they understand the coupling in your system.
The seam design. Where routing happens, how traffic is split, and how a single route gets rolled back.
A data ownership table. Every entity, its current system of record, how it is synced, and when ownership moves.
A characterization test plan. Tests that capture what the legacy system actually does today, including the odd behaviour your users quietly depend on.
A decommission plan. Named milestones for switching off old code and the old database.
Observability across both systems. Shared request IDs and dashboards that compare error rates and latency per route, old versus new.
Code and IP ownership in writing. You should own all of it from the first commit. At Geminate Solutions, clients own 100% of the code and IP, and you should expect the same from any partner.
If a vendor jumps straight to a tech stack proposal, push back. Choosing the stack is the least risky decision in the whole project.
For more on assessment, sequencing and migration options, read the legacy system modernization guide. If you want a second opinion on your own plan, Geminate Solutions will review it for free under NDA and reply within 24 hours.