On Legacy
2025-01-02
It happens. Developers sometimes claim that implementing a new feature is too difficult without rewriting the entire system first. Sometimes a new version (say, 2.0) of a product is released, with updated colours and buttons, but reduced functionality. Often at a considerable, almost mysterious, cost of this rewriting. But there are less expensive ways to deal with it.
We, software developers, call it Legacy. Legacy applications, legacy products, legacy dependencies.
It can be a legacy not because it's too old. It can be a legacy just because it's old enough. Here, in Information Technology, "it takes all the running you can do, to keep in the same place"©™. Everything is changing around. Constant change is the norm.
Your dependencies, libraries your application uses, are always updated. Primarily due to identified security vulnerabilities or discovered bugs. So, you have to constantly update the dependencies.
Your build tools, IDEs, frameworks are also frequently updated. Mostly because companies and corporations you rely on aim to stay modern enough. They run their own race, with competitors. They think you absolutely need all these new features coming with a new release.
Standards, protocols, and formats are constantly evolving. Who remembers XML? We don't want it anymore. JSON is preferred. So, instead of the Jabber, we should use the Matrix. Do you support the TLS version less than 1.2 on your web server? What a shame! It's totally insecure! (That's true.)
For example, keeping your personal Android app up to date can be challenging. Google raises the SDK version multiple times per year. And it forces you to use the latest SDK to be able to publish your app to Google Play. And I'm not even mentioning the switch from Ant builds to Gradle, and making Kotlin the main language, and incompatible changes in support libraries (to support newer SDK features in older runtimes), and incompatible changes in SDKs, and more and more security restrictions which require your app to explicitly ask permissions from a user. Google does great work to keep the Android platform backward compatible and secure, but...
So, any application you don't touch for two years becomes an unmaintainable bunch of code. You can run it, without modifications, but only on a two-year-old system. You can build it, but only with two-year-old tools. (And sometimes you even cannot build it as dependencies of your dependencies were updated, and now they are not compatible with your old build tool!) You can scan it for vulnerabilities and discover that tens of exploits exist to hack your system. And it's not hacked yet just because your app is not so popular, and it cannot give hackers money from an attack.
You must update dependencies, build tools, and, in the worst case, the protocols your app uses, just to continue development. The longer the time since the last touch and update, the greater the effort required to re-animate the app. It's better to apply these efforts constantly.
This is the running you must do to keep the app in the same place. To keep it alive. And you cannot avoid it. Otherwise, the app will die.
If you don't do these running exercises, the technical debt appears. You need to pay this debt sooner or later. The later you do it, the more pay you need.
What should you do if the technical debt is too high? If previous developers delegated it to you. Or if past yourself delegated it to present you. How to pay the debt?
You don't want and should not change the legacy code.
But you can divide and rule replace.
You cannot replace a legacy app with a new one, as it required man-years to develop the old app. Don't think you may spend less to make the new app. And during these man-years efforts, users of your app won't receive new features or fixes for existing bugs. This is a sacrifice any business would never make. And every developer should understand this.
You should split your legacy app into components. And then replace each component with a newer, supportable version. It'll be slow. It may take years. But finally, you'll throw away the legacy code and live with the new supportable system. Yes, you must take care of this new system not to become a legacy by that time.
This is the trick. Divide and replace by parts. So, you may keep your system running and actual, replacing the necessary parts at the same time. Of course, good architecture of the system, the parts it consists of, which can be replaced, becomes essential here.
Which components to divide depends on your system. These can be backend microservices, subsystems in the monolithic app, even separate classes. They can be frontend UI modules or components for different pages.
Sometimes even if you cannot split the code, you may separate how the code is called. For example, if you have a monolithic API, there are still different API methods. And you can replace method by method, one by one. You should add an HTTP gateway to your system which forwards requests either to the legacy app or to the new app based on the method. You will have the old and the new apps running in parallel. And even better, you may always switch back to the legacy implementation if something goes wrong with your new replacement.
Actually, the concept of feature toggle is closely related here. If you have a way to easily switch parts of your system which implement some feature, you are saved. Just replace the implementation with a newer one and switch into it.
In the frontend UI, you should find a way to replace the visual components from legacy ones into new ones. It can be trickier than with the backend API. Maybe you can replace page by page. Maybe you can mix together different components, and even different build pipelines. But again, you should be able to divide.
The idea is to gradually replace legacy code, one component at a time. You should be able to find and separate parts, small enough to be replaced easily, every time you touch the specific feature. Instead of trying to fix the legacy code, you replace it with a new implementation. Actually, it's how any development should work. Never update already written code. Create a new fixed version. This is what the O in SOLID principles stands for: "...open for extension, but closed for modification."
SOLID are principles of a good architecture (in object-oriented programming). Single responsibility means your components should be responsible only for a single thing. In this case, they are smaller and easier to replace. Open closed means your components should be open for interactions with other components or the external world, but closed for modifications. Think of the components as Lego bricks. You should not and cannot update a brick in your model, you have no such power. But you can replace one brick with another of compatible shape.
Liskov substitution means your components should be replaceable. Interface segregation means your components should have simple, understandable (documented?) interfaces. So it should be clear how to create a replacement. All the Lego bricks have the same pins and holes to be connected.
Do not try to maintain what is unmaintainable. Do not try to rewrite everything at once, it's too expensive and unprofitable. Separate a small slice which you can replace, and rewrite it. Do not pay for rewriting everything, pay for rewriting small parts when necessary.
And make sure to keep running for your new code, even to keep in the same place. Otherwise, it may become a legacy again, two years later.