← All writing
Craft · · 7 min

Strangler fig

The technique is one routing rule. The discipline is everything else, and that's where this fails.

Culture Agency

In September I wrote about a proposal to rebuild a client platform and what we did instead, which was to take the worst part and replace it behind the existing URLs. I said there was a name for that and I’d write it up.

It’s called the strangler fig, after the plant that grows around a host tree, roots down to the ground, and eventually stands on its own once the tree inside it has gone. The name’s been in circulation for about 20 years and the writing about it is almost entirely about large systems: microservices, event buses, teams with platform engineers on them.

None of which describes my work. So here’s the small version, which is the one I actually use, and which is mostly one config file.

The mechanism

There is a thing in front that decides where a request goes. Migrated routes go to the new system. Everything else goes to the old one. You move routes across one at a time, and when the old system has no routes left, you delete it.

That’s it. Everything else is detail.

At enterprise scale the thing in front is a gateway with a team looking after it. At my scale it’s a few lines in a web server config:

nginx.conf
location = /contact       { proxy_pass http://new; }
location = /about         { proxy_pass http://new; }
location ^~ /guides/      { proxy_pass http://new; }

location / { proxy_pass http://old; }     # everything else, unchanged
Nginx

Four lines and a fallback. The visitor sees one site on one domain. The URLs don’t change, the certificates don’t change, the analytics don’t fragment, and nobody outside the project can tell that anything is happening at all.

If you don’t control the web server, the old application’s own router will do it: a rule at the top that proxies certain paths onward. Uglier, works fine.

Which page to move first

Not the most important one. Everybody’s instinct is to start with the hardest thing to prove it can be done, and that’s the wrong proof.

Move something trivial first, ideally a page nobody visits. The point of the first migration isn’t the page, it’s exercising the whole pipeline: routing, deployment, the shared header, the analytics, the certificate, the 404 behavior, the redirect handling. All of that is where the surprises are, and you want the surprises on the terms and conditions page rather than the checkout.

After that, order by coupling rather than by importance. Leaf pages with no shared state first: about, contact, a guides section, anything that reads from a database and renders. Then pages with forms that post to something simple. Then anything with a session on it, last, and ideally all at once, for reasons below.

The seams are the real work

Here’s what the literature doesn’t tell you at this scale, because at large scale the systems are APIs and nobody sees them.

For a website, the two systems both render pages that have to look identical, and the visitor is walking between them by clicking links. So the header, the footer, the navigation, the type, the spacing and the cookie banner all have to match, exactly, for however many months this takes.

Three ways I’ve done it, in order of preference.

Share the stylesheet. Both systems load the same CSS file from the same URL. This is by far the easiest and it constrains the new system’s markup to match the old system’s class names, which is annoying and is much less annoying than the alternative.

Server-side include the shared chrome. The header and footer live in one place and both systems pull them in. Better in principle, more plumbing, and worth it if the migration is going to run for six months or more.

Or accept drift on low-traffic pages and migrate the visible ones in a batch. On a small site this is often the honest answer, and it’s why the terms page goes first.

At small scale the hard part isn't routing, it's that a person is clicking between the two systems and can see both.

How to tell it’s working

One number: how many routes are still on the old system. Count it, put it in the weekly update, and watch the direction.

It should go down every two weeks. If it plateaus for two months, that’s not a lull, that’s a signal that the remaining routes are qualitatively different from the ones you’ve done, and you need a plan for them specifically rather than more of the same.

The other property, which is the actual argument for the approach: at every point, you have shipped something. If the project is canceled in March because the client’s budget moves, you don’t have a half-built parallel system and nothing to show. You have a site where 11 pages are better and the rest are as they were, which is a perfectly respectable place to stop.

The way it fails

It stalls at 80%.

The easy routes get done, everybody’s pleased, and then the remaining six are the ones with the session handling and the legacy integration and the report that finance runs. Those are hard, other work arrives, and the migration goes quiet.

And now you maintain two systems, permanently, which is worse than either of the things you were choosing between. Two deployments, two sets of dependencies to update, two places to make every change, and a routing file that nobody dares touch.

Two things that help, both of them social rather than technical. Agree at the start that no new work goes into the old system, ever, from day one, so it can only shrink. And put a date on the last route, in writing, with the understanding that if the date is missed you have a conversation about it rather than a silence.

I’ve been on the wrong side of this. A site I worked on in 2019 is, as far as I know, still half-migrated.

Where it doesn’t apply

If the problem is the data model rather than the code, this doesn’t save you. Route-by-route works because each route is a fairly independent slice, and if every route reads from the same wrong schema then moving them one at a time just spreads the same wrongness across two codebases.

At that point you’re doing a data migration with a code migration attached, and that’s a different exercise with a different risk profile, and it’s one of the cases where a rewrite might actually be right.

Easy technique, hard discipline

Four lines of routing config, a shared stylesheet, and a rule that nothing new goes in the old one.

The pattern doesn’t fail because it’s complicated. It fails because after the first eight routes it stops being interesting, and nothing in the project is measuring whether the boring part is still happening.

Read similar posts
7 min

The rewrite that shouldn't happen

This is the third proposal to rewrite the same system in five years, all three from competent people acting in good faith, and the interesting question isn't whether they're right, it's why the proposal keeps arriving.

7 min

Scope creep is mechanical

A project finished about 40% over and there was no change log at all, because in-house doesn't have one. That turned out to be the most useful thing I learned all year.