Strangler Fig Applications ≠ Stable Projects
The purpose is exchanging the old constraints for new ones, rather than making the application appear more youthful. And they rarely go according to plan.
Engineering Teams modernise legacy systems for one reason only: to change its essential architectural constraints. Trading one set of properties that no longer serves the business for another to allow engineers to move faster. For a few years.
In 2013 I lead a special task force to de-risk our oldest parts of our backend. Pre-react, pre-microservices, this was a giant company with legacy systems still using PHP4 (I just aged myself there, thank you). This system was the touch point of high revenue features and user-facing analytics.
We—the engineers—identified these constraints leading to high maintenance costs. Maintenance costs that resulted in less than ideal compromises on what to prioritise and how quickly we can roll out new features to market while still having budget for observability and compliance.
Constraints pre-modernization
This backend system grew out of complex application as a result of merging two companies. This resulted in heavy friction in the following areas:
tech stack shared along a wider range than we’d like, making upgrades difficult
cumbersome coupling affected UI response times when deploying backend
boundaries where backend stopped and frontend started difficult to establish
maintained by multiple teams with different agendas due to its scale
Targeted Constraints
I gathered the main areas of improvements as a set of wish lists. Obviously this was all way too ambitious for the fast pace of the company, but the wider vision allowed pragmatism and efficacy to work together.
split up into multiple components to allow granulated deployments
maintain old interfaces, hiding network boundaries being introduced
start new components on latest tech and absorb functionality gradually
introduce tests using TDD for newly written components
This was the plan. It looked great and doable. Surely this will be a stable project?
Spoiler alert: it wasn’t.
Actual Outcome
I had an intuition that perfecting the entire would take months. We had a few weeks. So I gathered the engineers in question and we set out to triage the main objectives. Here’s what we actually delivered, in order of completion as far as I can still remember:
1. TDD for all changes in the new subsystem
Not just newly written components. The team had no experience with BDD or behavior decomposition, so the initial version was rough—heavy mocks, legacy interfaces. But it was a necessary painful step to give the business more confidence in our practices.
2. Extract a single component for Critical User Journey to act as a high-frequency learning tool
Split up into multiple components to allow granulated deployments
There was no way we were going to split up the entire backend equally. So we set out to extract the most critical subsystem and keep it small so we could iterate fast on it. We ran into issues of upgrading vendor-related dependencies, custom compiled modules so feedback loop times got a higher priority than scope.
3. Create a simple SDK for opt-in usage of new functionality
Maintain old interfaces, hiding network boundaries being introduced
We immediately saw that fully backward-compatible usage was not going to happen. Migration was going to be painful either way, so it had to be quick, sharp and mess free.
First two days we pivoted our efforts to create an SDK that mimicked old scenarios fully showing the network boundary explicitly. This used a combination of JSON-RPC and interface-sharing in Subversion submodules. Industry was still using XML-SOAP2.0, so our approach was novel at the time.
4. NEW: Containerise the new dev environments to allow for rapid onboarding of new engineers from other team.
Start new components on latest tech and absorb functionality gradually
Mind you, we didn’t just contain the services. We containerised the entire IDE and debugging environment.
It turned out that the old system was so heavily bound to the working desktop of each engineer that nobody could work on both at the same time without major re-installation of components.
Did I mention this was pre-docker?
Containerising the IDE itself using X-forwarding in Linux allowed everyone to do hit-and-runs in key places of the new stack whenever they had a minor change request that they could do themselves with minimal training.
Of all the changes, this one we simply couldn’t see coming. Developer Experience is key for any modernization attempts and 10 years later when coaching teams I still see it being one of the main reasons for developer churn and low productivity.
Credit where Credit is due
The old system was messy. We all contribute to its creation and eventual sunsetting. The new system would also at some point become legacy. Code doesn’t age. It’s the constraints of the software that become obsolete over time.
I couldn’t have done any of this without the talented team I was working with: Taj, Alen, Metod, Ahac, Aleksander, Gregor, and Davor.
Martin Fowler coined the term ‘strangler fig approach’ in the software context. He recently updated his article about it and I invite you to consider his recommendation for legacy software displacement:
Ian Cartwright, Rob Horn, and James Lewis defined four high-level activities that we need for this kind of incremental approach.
Understand the outcomes you want to achieve
Decide how to break the problem up into smaller parts
Successfully deliver the parts
Change the organization to allow this to happen on an ongoing basis
I help engineering leads cut through the noise to get teams on track to greatness.
If you find Crafting Tech Teams and this article valuable please share it with your friends and coworkers.
I worked on this project, which was done partly out of necessity.
A crucial third-party vendor library we were using stopped supporting old versions of PHP, which meant we would have to upgrade the monolith.
That would be a massive amount of work. Unfeasible.
So Denis devised an approach to identify the application's seams and start separating functionality from the monolith into services that lived independently of the main system.
It increased complexity, as calls now went over the wire. Still, we gained the freedom to run different environments, and with that freedom, implementing business-valuable features became possible.
To counteract the increase in complexity, we pimped out the dev experience.
One positive outcome was that changes became easier.
With more code separation came fewer side effects, and we were able to start writing tests, which provided a sense of reassurance about the project's progress.
I vote that the next article explains the: "Doctor" concept we successfully deployed ;)