The strangler-fig pattern is simple to describe: put a router in front of the old application, rebuild one slice behind it, send that slice's traffic to the new code, repeat until the old application is empty. Most strangler migrations that stall do not stall on the rebuilding. They stall on sequencing: the first slice was chosen badly, or the second slice needed data the first slice should have moved, and the program loses its sponsor before it shows anything.
This is the method we use to sequence a WebForms migration. Most of it applies to Classic ASP and, with different plumbing, to VB6 and FoxPro desktops.
The router comes first
Before anything is rebuilt, put the routing layer in production with every path still going to the old application. On IIS this is usually a reverse proxy (YARP in a small ASP.NET Core host, or ARR) in front of the WebForms site. Running it for a week with no behavior change proves the plumbing, gets it through change control once, and means the first real cutover is a config edit rather than an infrastructure project.
Bridge the session early
WebForms applications lean on Session and Forms Authentication. The moment two applications serve one user, you need both to agree on who is logged in and what is in the session. Options in order of preference:
- Shared authentication ticket. Configure the new ASP.NET Core application to read the legacy Forms Authentication cookie (there is a supported compatibility path for this) so a login on either side works on both.
- Externalize session to SQL Server or Redis with a serializer both sides understand. Painful if the old application stores arbitrary objects in session, which it does.
- Narrow what the slice needs. Often a slice reads two session keys. Pass those explicitly through the router as headers and stop pretending the whole session is shared.
We almost always end up with the first and third together.
Choosing the first slice
Score candidate slices on four things:
- Separability. Can the router identify the slice's requests cleanly by path? Does the slice have its own pages, or does it live inside a master page with four other things? A slice with a clean URL prefix and few shared controls is worth far more than a valuable one that is tangled.
- Data ownership. Does the slice own its tables, or share them? Slices that mostly read shared data and write to their own tables are ideal. Slices that write to tables everything else reads should go later, once the readers have moved.
- A real owner. Someone in the business who uses the slice daily, will test the pilot, and will tell you within an hour when it is wrong. Without that person, cutover drags for weeks.
- Visible value. Something the sponsor can show. Not necessarily the most valuable module, but one where "the new version is live" means something.
The typical winner is a maintenance workflow: customer, product, or vendor maintenance. Clean paths, mostly owned data, daily users, obviously improved when rebuilt. The typical loser is order entry: highest value, most shared data, most undocumented rules. It goes third or fourth, once the harness and the data plan have matured.
The shared-data problem
Both applications will read and write the same database for most of the program. Rules that keep this from becoming a mess:
- One writer per table. When a slice moves, the tables it owns become write-only-from-new. The old application may still read them. If the old application also writes them, that write path is part of the slice and moves with it, even if it lives on a different screen.
- Do not refactor the schema during a slice. Rebuild the slice against the schema as it is. Schema changes get their own, smaller, slices after the code on both sides is under test.
- Triggers and stored procedures with side effects are the hidden coupling. Inventory them in the map before sequencing anything. A trigger that updates an audit table is easy; one that recalculates order totals is a slice in itself.
- Access and FoxPro data that another process still writes cannot be moved until that process is either migrated or pointed at the new store. Find those processes early. There is always a nightly job on a server nobody remembers.
Why reporting goes last
Every sponsor wants to migrate reporting first, because it is visible and because the old reports are ugly. Resist. Reports read everything. A report slice depends on every table and every rule in the system, so it can only be reimplemented correctly once everything upstream has moved, and until then the old report is the thing the characterization tests compare against. Keep it running, point it at the same database, and migrate it when it is the only thing left in the old application. At that point it is usually small.
What "done" looks like
A slice is finished when: the harness passes against the new implementation; the router sends 100 percent of the slice's traffic to it; the old code paths for that slice are deleted from the legacy repository (not commented out); and the rollback has been tested once by actually rolling back for ten minutes. That last one is skipped constantly and is the reason the third slice's cutover is scary when it should be routine.
Sequencing is the part of a migration that most needs a senior engineer who has done it before, and the part the tools do least for. It is also the part our codebase assessment is designed to settle before anybody commits to a slice.