I would like to share one of my favourite success stories on software modernization. It is a case study of a legacy web app that we managed to turn into a highly scalable SaaS in just 2 years. Could this be done faster? Of course. But the goal was not only to pay off the technical debt and make the app scalable but also to do so while maintaining the constant delivery of the new features at the same time. And of course, ensuring the full operability of the business processes. No feature, code or development freeze, as users and competition wouldn't wait. Is it possible to get rid of technical debt from the legacy system, modernise the app and keep up with the competition with all the new stuff at the same time? Sure, that is what we did, and I would love to share with you how.
Legacy system for classified ads (SaaS product)
The story starts 7 years ago when we first met our future client from Scotland. By then we were focused on building web applications from scratch - so-called greenfield projects. Mostly for start-ups around the world. Hard to even count how many "innovative" and "groundbreaking" product MVPs we have built by then. It was fun, no doubt, but you probably know the stats — how many startups survive after the first few months or years? Not many. But we definitely learned a lot on how to, and how not to build online products by then.
Back to the Scottish client — this case was different, as they came to us with the already existing product. A good one, I would say, and they were already a branch leader in their country, having many important and big clients. That was something new for us, not a startup, but a solid business, doing pretty well on the market. Growing organically, instead of struggling from one investment round to another. That did impress us.
They started their business operations many years before we met, and originally their product wasn't SaaS software at all. It wasn't even a digital thing at the beginning. Do you remember old-school Yellow Pages books? With contact details of people, institutions and companies? That is more or less what they were doing at the beginning of their business operations. It was a printed book with branch-specific classified ads and contact details. Sold and delivered in a subscription model. Not online — but via traditional snail mail. Many companies started like that, just to mention Netflix, which originally was a mailbox subscription of DVDs.
But they went through their digital transformation, from printed book version to WordPress website, and then to SaaS software. When we met, their product was already a fully-fledged online tool. They demoed a software to us, and it was not only packed with useful features but also pretty good-looking. Someone did a great job developing this SaaS - we thought. So, why did they come to us?
All the charm of legacy applications
Their goal was to develop their product further, add some new features and prepare their SaaS to expand from Scotland to the whole UK. Which is a 12× bigger market within their branch. This all sounded pretty good. A former startup that did well on the market, already achieved business success (at least to some degree) and wants to take the next step with their product. Perfect, so where is the catch?
Their demo of the software was impressive, and we were pretty sure that its users must love it. And they partially did, but at the same time, there were some complaints. Happening more and more often. Users complained about the app getting slow at times. Bugs started to show up more frequently. Even about their sleek UI, which wasn't as responsive as it was before. Besides, new features were developed in months, not in weeks as before.
The more they told us about it, the more apparent it became to us that they are dealing with typical issues of ageing legacy systems. And that their business success was fuelled by a huge debt. But not a financial debt, but a technical one.
Tech debt in legacy systems
Rapid development at the beginning of product growth usually requires cutting corners here and there. It is perfectly fine, this is how you develop apps in the early stages. And this is exactly what we call a tech debt. It makes no sense to invest a lot of development hours and money in software and features that are a bit vague and the company is still looking for a solid business model. Pivoting is very common, thus startups need to be agile and lightweight. To some extent, the more corners are cut, the faster the initial development. But after business objectives and strategy solidify — software should follow, and tech debt should be repaid. Otherwise, you may get into a real debt spiral. Technical one of course, but very similar to the liabilities you may owe to the financial institutions.
In the case of our Scottish client, this moment was missed and the future growth of the product became blocked by the piles of unpaid tech debt. Debt that initially funded their quick digital transformation from printed books to SaaS. Moreover, as it turned out — their previous developers, simply admitted that they are no longer able to develop this product further. And resigned. Leaving our future client alone with the product.
Was this legacy application that bad?
After our CTO took a quick look under the hood — we understood why did they resign. Although the software product was indeed impressive, at least visually, it had some real skeletons in the closet. A whole army of skeletons I would say.
Their existing code was indeed a great example of something that we call legacy code. Starting from the code structure and versioning. A branch standard in software development is using GIT — which keeps a log of all the changes made to code, so that these can be traced back and inspected whenever needed. But here is how they approached the code versioning:
Instead of having a one-page footer file and its history in GIT, they literally copied this file whenever a new version was needed, and renamed it to something different - version v1, v2. v2new, v2new2 and maybe v2evennewer — why not?
It reminds me of times when there was no cloud, and Word documents were flying around via emails and each edit was then sent via another email message with the file name changed. That is not how you do it nowadays (it is not, isn't it?), and that is not how you do coding since... as far as I remember.
But there were way more issues with this legacy software, just to name a few:
- They thought their code used some framework, but it didn't. Instead, we found some leftovers from different frameworks, none of them was used as it should be. And a few pieces of other, even older systems, perhaps some early versions of this software (?). Mess... a big ball of mud of some custom PHP code glued with random pieces of external libraries copied directly to the main codebase.
- Speaking of copied code. This legacy system was a perfect example of something that we call copy-paste-driven development. The same lines of code are copied over and over again, all over the app. Why is it problematic? Because sometimes even the simplest change has to be done in dozen of places, instead of one. And it is very ommission prone as well.
- Security-wise - hardcoded keys, passwords and access tokens directly in the codebase. That is simply not how you keep your sensitive data and secret keys - a very non-secure approach. Better not show it to your chief information security officer.
- And process-wise — their deployment was very... old school. You may (or may not) remember the FTP protocol, used in '90 (?) to share files to servers. It was made for file storage, never for software deployment. But back in the ancient past, this is how many websites were actually deployed. And in 2015 our Scottish legacy application as well.
This list isn't complete for sure, but I think you already get the point. A serious intervention was a must-have to rescue this legacy system. No small tweaks or improvements would make any difference here, we had to approach this from the ground up. Code-wise, architecture-wise, security-wise and process-wise.
Legacy application modernization - what are the options?
When our customer approached other software development companies, most of them suggested to rewrite this legacy system from scratch. Get rid of existing code completely, and implement the whole UI, database and business logic from the beginning. A complexly fresh start sounds tempting, but that was a no-go. This app was being developed for a few years, and rewriting it would take at least one or two. End users won't wait for new features that long. And neither will the competitors. So... how to rescue this outdated software?
Let's have a quick look at the possible approaches to application modernization. I will give you just a brief summary of the potential options, but if you want to dive deeper into this topic make sure to read — It's Time to Handle Technical Debt in Your Legacy Application—4 possible scenarios.
Option 1 — keep on with the legacy system development
We have a legacy system, but it works and it earns its own keep. Let's try to develop it as it is without groundbreaking changes. What are the pros and cons of such an approach?
- Pros: no refactoring cost, all the work is put into the delivery of new features, with just minimal changes to the process new features can be developed from day one
- Cons: tech debt will continue to grow, the development pace will continue to decrease, the app will be stuck with outdated technology, security vulnerabilities may cause serious problems one day, and high maintenance costs growing over time
Option 2 — modernize legacy systems by slow refactor
Cons from the previous options are pretty severe. Let's address them by adding a slow code refactoring to the process. We will still work with the existing code, but instead of dully continuing the bad practices, we will try to use good ones (if possible) and try to tidy up the code while implementing new features. No groundbreaking changes, but at least some small improvements over time.
- Pros: the ability to introduce features after a short period, the code gets slowly improved
- Cons: still not able to use new technologies, getting rid of technical debt will take ages, security vulnerabilities still may cause issues unless secured in the first place, overall slow development pace
Option 3 — rewrite the legacy system from scratch
If both previous options have so many disadvantages, maybe it is indeed a good idea to rewrite the outdated system from scratch? It will allow the replacement of the underlying technology, enable new and better capabilities and will be probably the fastest way to get rid of all the legacy code.
Sounds tempting? Indeed, but usually, it is not a good idea at all. I won't get too much into details as we have a separate blog post on 5 reasons why rewriting an application from scratch is a bad idea. Let's just have a brief look into the pros and cons of this approach to rewriting outdated systems from the ground up.
- Pros: the ability to use modern technologies, no more old systems and tech debt - only new code and tech stack
- Cons: long time before any new feature will be delivered to users, customers and competitors won't wait, prone to reappearing errors that were solved in the past and lack of complete specs/documentation may lead to omissions and malfunctioning features, huge overall cost and time before any value is delivered to users
For relatively small legacy applications it may be an option. If you are able to rewrite the whole app in a month or so, you probably should do it. But the larger the legacy software is, the less feasible this option is. Business objectives have to be fulfilled, business performance has to be maintained, and this generates new requirements for the software. No time to wait for the rewriting process.
So, is there any other modernization approach? Or are we doomed to maintain the old legacy systems forever?
Our approach to the modernization of legacy applications (option 4.)
None of the previous options was viable for our Scottish customer. Their app was too big for a full rewrite from scratch, and sticking to the legacy code was too painful and too risky.
The goal was to find a solution that will allow us to use a modern technology stack, get rid of legacy code in a reasonable time and continue to deliver new features to users without any delays. Sounds like the holly grail of legacy system modernization? Probably! But this one actually exists!
Strangler pattern approach - have your cake and eat it too
By then we didn't know a name for this approach, we just figured it out ourselves and learned afterwards that this method was already named as Strangler pattern by Martin Fowler (a key figure in software development society). This approach in a nutshell assumes that we create a brand new application not instead of the existing one, but next to it. So instead of one app, we have two apps connected together. All the new features are implemented in the new one (using modern solutions & technology) and existing features are gradually rewritten and removed from the legacy system.
With time, new application will grow and the old one will become diminished, or... strangled. This is exactly the way to modernize legacy by strangling it. The name was actually taken from the Strangler Fig — a parasite that grows on trees. It is growing year by year until it strangles and replaces the original plant.
So the new app slowly strangles the old one, the same way as the strangler fig replaces the original tree. I won't get too much into the technical details of this approach. We have separate blogposts for developers about strangler pattern in practice.
Pros and cons of gradual legacy system modernization approach
This approach has huge benefits which in most cases outweigh the downsides — let's have a quick look.
- allows to pay back tech debt and gives control over how much to invest in it
- new features can be added just after a few days of initial preparation
- relatively easy to implement after the initial setup
- makes delivery of new features way cheaper and faster
- allows the use of new technologies
- some initial work is required to connect existing legacy software with the new platform
- development environment has to be prepared for both parts of the system (old and new)
- paying off all the debt may take months or years if the business decides to focus only on new features and neglects the process of paying off the debt
Also if you want to dive deeper into whether this modernization approach is the right solution for your digital transformation make sure to read this summary about strangler pattern approach to migrating applications - pros and cons, written by our CTO. It is not the only option, there are more that we practised in the last few years, here you can find another story written by one of our senior devs - Legacy code - strangle or tame?
How did we start with legacy modernization?
Our Scottish client seemed relieved when we presented this approach. Later we learned that all the other companies he talked to suggested rewriting the app from scratch. Having to freeze the development of new features until everything is rewritten? Investing a lot of time and money to get exactly the same product? Risking to repeat the same problems that were solved in the past? No, it wasn't an option. On the contrary, the strangler pattern seemed to fit very well. And it did indeed.
Although this approach to application modernization allows us to start with the development of new features relatively quickly, there were a few things that we had to secure and solidify before we could work on the new stuff. There was also some prep work to be done to let the two apps work in parallel. Let's have a look how did the first week and month of cooperation with our Scottish client look like.
First of all, we had to take over their legacy code and introduce proper code versioning (via GIT and GitLab) — to be able to track all the changes and versions. We also used a few tools that automatically analyse the code (so-called static analysis) that provide us with useful statistics and insights about the legacy application codebase. For example, it helped us to understand the code structure and see where is the most complexity hidden.
Knowing a little bit more about the skeletons in the closet, the next step was to introduce some basic automated tests. One of the big problems of legacy systems is that even the simplest changes in their codebase can result in unexpected errors and issues. We had to be sure, that if we change something, we won't break anything at the same time. Having the most important parts of the legacy system covered with tests that can be executed automatically gave us more confidence and predictability.
Another important job to be done in the initial days was the so-called containerization of the project. We used Docker for this, and if you don't know what it is I have an article on What is Docker and why to use it? — it explains it in very simple words. Containerization of the legacy software allows us to have the whole app bundled into unified boxes (containers), that can be easily opened on any server or developer's computer. It saved hours of complex setup on all the devices. And when we are dealing with legacy software - setup can be a real nightmare. You can also check how Docker reduces development costs.
Next, we extracted all the secrets (like passwords, tokens etc.) from the code and placed them in proper vaults and secret storages. So that no one can by accident or on purpose access any of the production servers, databases etc. We also removed some hardcoded settings from the code, and prepared configuration files that allowed us to have different config values on local devices (developers computers) and different on test and production servers.
Last but not least, we selected a new tech stack for the legacy-free part of the app — including a few modern systems, libraries and frameworks. And of course, we implemented a basic setup for our strangler pattern. The main goal of this initial setup was to be able to define which features will be served from the legacy system and which from the new one.
It was a productive week, a lot of work for sure, but we ended up with a development environment ready to start some serious changes and improvements in the following weeks. The new part of the app that we created was completely empty by then, but it was ready to be filled in with legacy-free features in the upcoming weeks and months.
But before we jumped into coding new features, there were a few more urgent action items. Of which the most important topic was security.
Code analysis done in the first week showed us that there were some holes in security that could be used to break into the app. We also did a few manual checks and discovered some other security vulnerabilities. It would be too good to be true to start implementing new stuff just after a week after the project commencement, but unfortunately, we had to take care of these critical issues. Although this Scottish app has never caused any data breaches, this was a potential threat, and we didn't want it to happen on our watch. It took us around 2 weeks to ensure that all the security holes are fixed.
The second half o that month was spent on performance optimisation. Although the app was working well in a test environment, it was way slower on the production server. And the more users were online, the more noticeable was the delay, breaking the overall user experience. It also had a negative impact on the employee productivity of our Scottish client — the delays were slowing down the work of admins moderating the classified ads of the end users. We conducted some load testing and it confirmed that the overall app is able to handle a relatively small number of users at the same time. So before we decided to build any new modules or features for that app, we had to optimize a few things. There are usually at least a few things we can relatively quickly improve, you can learn more about these in the article on the Top 5 hacks to fix slow web applications. Here we used tools called profilers, to investigate step by step all the resources consumed by the code execution, which allowed us to identify the bottlenecks. Adding caches, creating database indexes and moving static resources out of the main servers were just a few optimisations we did. We also optimized some database queries and got rid of unnecessary code executions. In the end, after just 2 weeks of work, we were able to handle almost 10 times more users! And that of course improved in the upcoming months.
Last but not least, we configured continuous integration pipelines, so that every time we implemented some changes they were automatically verified and prepared for being released to the test server. We also prepared the deployment scripts, which prepared us for the very first release to production (finally, no more deployments via FTP!).
After this month we were ready to go live — although there were no new modules or features, the app was way more secure and able to handle more users. Nothing that would bring a competitive advantage, but a solid foundation for what's to come.
The first year of legacy modernization
The first deployment to production was very smooth. It is always a bit more challenging when you release an app that contains almost 100% legacy code for the first time, so we were really happy that it all went very well. After all the prep work done in the first month, and after a successful deployment we were ready to start building something new. It was finally time to implement some new modules and at the same time begin our migration process from legacy to a fully scalable SaaS.
More features & less debt
I remember pretty well the very first feature we implemented in the legacy-free part of the app. To this day it is one of the most frequently used modules of the app and the very first screen that every user sees after the login. It is simply called "The Feed" and looks more or less like a Facebook-type feed/wall of information, that summarised all the actions happening in the app. Before that, users didn't have any way to see what changes or tasks were done by the other teammates. Feed allowed them to be quickly updated on all the important actions. After three weeks of work, the very first module was released to production, providing new value to the end users. This module maybe wasn't the biggest in the app but allowed us to test the approach of having two separate apps (the old legacy system and the new part) connected into one platform.
The second module that we worked on was a list of categories that users have access to via certain permissions. Something that was already existing in the app but our Scottish customer wanted to refresh its look&feel. It was a good opportunity to get rid of this module from the legacy part of the app, and on the occasion of the redesign also rewrite it and recreate it in the new part of the system. This is how the actual app modernization started — the first feature was moved from the legacy realm to the advanced technology one.
Our backlog for the first year contained a lot of features like these two. We were planning to refactor a few APIs, modernize legacy systems responsible for integrations, add some advanced analytics, restructure some data structures and update the design of a few screens. But the approach was more or less the same — all the new modules were built only in the modern part of the app, and old ones were gradually moved from old to new. This allowed us to deliver new tools increasing the competitive edge of this SaaS, and pay off the tech debt at the same time.
This migration process was gradual and quite smooth. We used techniques like canary releasing and feature toggling. Both allowed us to reduce the risk of introducing a new code by slowly rolling out the changes to a small subset of users instead of all at once. And if anything goes wrong, it allows you to switch the changes off. But fortunately, we didn't have to use that switch too often 😉
More clarity and visibility
During the first year, we also focused on so-called observability. In short — it is everything that allows us to monitor the whole SaaS software and ensure it stays healthy. It included uptime monitoring, automated error tracking and performance monitoring. I won't get much into details here, but if you want to learn more on this make sure to read my post on SaaS monitoring — 6 things you should monitor to ensure your SaaS is healthy.
Last but not least — some more security. Although in the very first week, we already took care of the most critical issues, there were still some actions we could perform to make sure that our Scottish SaaS is safe. For example, we installed tools that automatically check all the libraries and third-party tools we used against known threats and vulnerabilities. We also did some penetration testing and added a cloudflare.com as an additional layer of security in front of the whole app. And to sleep tight, we ordered an external security audit, to confirm that the app is properly secured.
The first year of legacy transformation was finished with a bunch of new modules and tech debt was reduced by around 35%. This a pretty good result that gave us the confidence in our modernization strategy. All of that with completely no downtime for the SaaS app and no freeze for the business processes.
The second year of cooperation
It was time to take the next step and focus on another important aspect of SaaS software — scalability. We knew that our Scottish customer was aiming high, and sooner or later we will have to handle way more users and data. We were already capable of delivering new features, but were we ready to have new users? No, not really.
Although getting more users is good in general, this has to be carefully planned and the software prepared for it — otherwise new users can even kill your SaaS. So the crucial part of this legacy transformation was making the whole SaaS tool scalable and ready to handle more users in new markets. Unfortunately, it wasn't that simple, because the legacy application that we started to modernize had a monolithic nature. It was one big block of software that couldn't be scaled horizontally, and the only way to handle more users was to use stronger servers. But this has its limits.
This is rarely a good option, so instead, we focused on getting rid of all the scalability blockers and making this Scottish SaaS ready for true scaling (a horizontal one). To do so, we had to introduce sessionless authentication, filesystem abstraction and queuing systems. I won't get too much into detail, as it is a topic for a separate post, or book perhaps 😉. But we ended up with an app that can be easily launched in multiple instances (copies). The more users, the more instances. Simple as that.
The last important job for the second year was the migration of the infrastructure. To gain real flexibility in scaling, we decided to use a cloud solution — in this case Google Cloud. Using cloud computing and cloud hosting is not always a necessity. But in many cases, it makes scaling way easier and more flexible. It allows you to increase the number of instances of your app on the fly, even during the day, when there are more users in peak hours, you can make use of additional resources. Without having to purchase additional physical machines (servers). If you are not sure if your app will benefit from it our CTO wrote a good piece on Should I host my app in the Cloud.
Legacy modernization - summary after 2 years
Two years of legacy application modernization were sufficient to transform this app from legacy software to a highly scalable SaaS. Although the app still contained a large portion of legacy code, we were able to deliver a lot of new modules for the end users. And processes that we have introduces in the first weeks of cooperation initiated a constant decrease of technical debt (ca. 35% decrease each year) and ended up with a final deletion of the last piece of legacy application 4 years later.
But just after these first two years maintenance costs were reduced by 70% and the development pace increased a lot — shortening time-to-market for new features by almost 87%. This enabled both — business and software growth.
Since then we specialised in modernizing legacy applications making it our core business focus. We learned a lot about how to evaluate legacy systems and how to move forward with their development. While many companies want to take only greenfield projects (building software from scratch) we focus almost purely on legacy application modernization.
So if you need help in this area, just drop me a message on https://accesto.com/contact/
Disclaimer: some of the business details in this story were changed due to NDA reasons, but the essence of this case study remains unchanged.