When talking about legacy code, have you ever heard someone say we're gonna strangle it!? This article is for them. The strangler pattern is a powerful way to gradually rewrite a software system while still running the old system. However, words matter and language shapes the way we think. Also, let's be real, calling it the ivy pattern never really caught on.
I'd like to propose the Chrysalizer Pattern. You can now say we're gonna chrysalize it.
Here's how it works
As the Hand of Nature...
- You put the legacy code into a chrysalis
- You create a new, beautiful garden around it
- When ready, the legacy code flies away as a butterfly
First, let's discuss some alternatives. If the architecture for a legacy system is decent (or just really big), you may choose to replace one part at a time, a.k.a. the Ship of Theseus Pattern. This lets you make smaller, less risky rewrites and get them in front of users sooner. As an example, Firefox engineers replaced key components with Rust code for speed and safety.
If the architecture isn't stellar or the programming language itself is a bummer, you may choose to rewrite the whole thing all at once, a.k.a. the Big Bang Pattern. If an experienced architect is advising the project (I hope so), this can be rewarding but it also incurs the most risk and will probably take way longer than you think.
An alternate approach to rewriting poor architecture is the Chrysalizer Pattern, where you isolate the legacy system and gradually replace it with a new one. While the rewrite is in progress, you'll be running both in parallel.
Enter the chrysalis
Putting the legacy code into a chrysalis just means you don't touch it unless you absolutely have to. You might need to fix bugs -- that's OK -- but you should keep new features to a bare minimum.
You can designate an owner to enforce this and make exceptions when necessary (and with good reason). If there's business pressure to add new features to the old system then the Chrysalizer Pattern is flexible enough for that but it will prolong the migration and you might have to add features in both systems.
Create a new, beautiful garden
Plant seeds. Let flowers bloom. Make it beautiful! This is what I like about software rewrites. You are free to build the optimal system you need and want. If you can dream it you can build it.
It's your chance to not only fix the architecture but to optimize for productivity and even optimize for fun. By fun, I mean the fun of coding: fast tests, fast tools, better abstractions, rapid iteration -- this is the recipe for innovation.
In my experience, you only get one chance to truly optimize for fun and that's at the beginning of a new project (including rewrites). It's hard to make a business case for stopping feature work in the middle of a project so that your team can, oh, take a breather and optimize for fun. However, you can do it along the way when building a new system.
There are some special companies that can afford to dedicate entire teams to developer productivity. If you work at one of those, congratulations.
There are various techniques for running a new and old system in parallel while migrating. If it's a web application, you may be able to use a reverse proxy (nginx, for example) to migrate one URL at a time. The first URL will be the hardest but after that you'll pick up momentum. You've probably even seen this in the wild where you click deep into a site and suddenly it looks a little weird.
I've had success with first migrating low traffic or low complexity URLs to gain stability then tackling higher traffic ones with confidence. When my team migrated addons.mozilla.org from jQuery + Django to React + Django REST Framework we started with the mobile site where traffic was lower. Once we ironed out the kinks (let me tell you about SSR), we began migrating the desktop site one URL at a time.
There are a lot of things to consider: shared auth, UX between old and new, complex problems you forgot about, etc. If UX inconsistency is a huge concern, you can also choose to hide the new system behind feature flags until ready. Going too deep into a new product without real world usage is a risk in itself, though.
If the system is modular enough, you might be able to simply put new code in a separate folder and add some kind of adapter from old to new. This is pretty much how React Native suggests migrating from native, i.e. one view at a time. It helps to have a well supported adapter, of course, otherwise you're taking a huge risk.
For backend systems, you may be dealing with a monolith of legacy code. A Chrysalizer approach could be to replace one chunk at a time using microservices. However, once you have a web of microservices you'll then be debugging microservices (a separate problem).
You will reach many milestones in your migration, such as finally migrating the landing screen (yay!). There will be stragglers and they will linger, especially the low priority features. Eventually there will come a day when you've migrated everything.
Great success! Raise your hand up slowly then bring it down hard on that shiny delete button. Delete the code, delete the repo, feel the wind in your hair, feel the sunshine of a brand new day warming your smiling face.
The legacy code is now free to fly away and never be seen again. Good luck out there, little buddy! Nothing got strangled, no one got hurt.
Next time your colleague wants to strangle some code, kindly suggest that they chrysalize it instead. The legacy code itself was written by humans, after all, and everyone makes mistakes.