Keep Legacy Code or Rewrite? A Middle Way

Sometimes a powerful loader is all you need

Francesca Milan
THRON tech blog
8 min readJul 21, 2020

--

Legacy code or rewrite knockoutjs vue components applications

We began to write our main product in 2013. That was just our second Single Page Application (SPA) ever. The first one was a small project, we analyzed the smaller experience to avoid repeating errors we already made.
At the time, the browser and js libraries landscape was different, our enterprise target included IE9 users and we were not confident about big and complex frameworks so we preferred to adopt a set of separated libraries. We built our own framework: flexible, easy to use, and with just the features we wanted such as data binding, templating, routing, internationalization, …
For each missing feature, we were free to choose the preferred library, with its pro and cons. It was a case of “too much freedom of choice”.

Part of our journey in splitting the monolith application is available in other blog posts: code repository impacts, frontend code architecture.

As time went by, we added many features to our application, we become more skilled, the tech landscape evolved but, most of all, the application become bigger and bigger. We tried our best to keep the highest code quality we could but, as you know, any developer that reviews his code after many years wants to burn it and rewrite it. With a big SPA, this is just not possible because of the risk and the size of the task itself. We had to find a different way to satisfy our new requirements:

  • we wanted to be able to add new sections to the app without affecting performance. Our SPA did not follow “code splitting” so each new interface was added to the single fat chunk of code. We have been focused on adding features for too long and when we realized that it was the moment to work on code-splitting, it was too late: it was all too tightly coupled to be split.
  • we wanted to be able to adopt modern and faster frameworks but, more important, be free to update or change them over time. For many complex interfaces, 7 years old libraries don’t deliver the same performance that can be achieved by using modern ones that leverage, for example, Virtual DOM.
  • By collecting data on our analytics we realized the market was mature enough to reduce the scope of the supported browsers to IE11+. This opens up possibilities that were just not possible while keeping IE9+ support.
  • We wanted to be able to split code development and maintenance across different team members working at the same time. Business does not slow or stop, so we still want to be able to push out new features at a high pace, but we want to be able to do that by using new libraries, so we had to find a way to make old code coexist with new code.
  • We wanted an easier and more agile development. In the current architecture, a developer that needs to add a section might have to learn about the whole architecture, to ensure they don’t introduce regressions. The solution we are looking for must allow for better decoupling of components and to let each developer reduce their learning scope to just the areas they have to work on.

But how can we reach all these objectives without stopping product evolutions? It would be impossible to rewrite the entire application for a single release.

The approach we used

We split the work into three stages:

Step 1: start from scratch, like we were going to redesign everything

We tried to break up with the current application and start to imagine a new architecture from scratch, looking at popular frameworks and famous applications to get inspiration.

Then we created a Proof of Concept (POC) solution to validate our requirements and while we were developing it we added another requirement: it has to provide section-management framework independency. It means that we wanted to avoid being tightly tied to a specific framework. A time will come when we will want to adopt a new framework for special futures or improve maintainability, and we want to be able to do so without refactorings.

The POC idea was to have a “loader” that could import each section with the lowest amount of dependencies possible, up to the point that we wanted to import sections written in different frameworks. The old application, in this case, would just become a section to be loaded.

This was the only way we found to modernize our application without stopping product evolution: consider any old interface as a special section of our new and shining application.

With these requirements, the core of the POC has become a “section manager” that grows around the Vue Router and its powerful navigation guards. When a user asks a section we check its capability and load the right section chunk.

The old application structure can be summarized as:

legacy code, loading process old application
The loading process of the former application

To allow the old application to be loaded as a module some changes needed to be made:

  • The old application bootstrap was a lightweight module that manages basic behaviors about application startup such as resources loading clip, mobile application redirect, brand colors, security preferences. This was the only part of our SPA that has been totally rewritten. Those were not too many statements, so we also took the chance to remove deprecated logic.
  • The old application used the entire HTML body to render the interfaces. This approach had to be changed so that it could work on a given HTML Element instead of the whole body section.
  • We were forced to change the navigation API. The legacy application used window.location.hash instead of modern window.history.pushState, used by Vue Router of the section loader. In the first phase we tried to make them work together but, adding the IE11 support, we obtained a “monster” that was not even working consistently. The only solution we found was to inject the reference to the main router into the just-loaded section so that the entire application navigation uses the Vue router.

We then stumbled into a problem: the new application was allowing us to create only news sections, which was the smallest entity that could be created and we thought that it might have been just too large in some areas. Why did we just stop at the “section level”?

We then identified the three different scenarios that we wanted to be able to manage with the new architecture:

Creating new sections of the application is not very common, adding new subsections is much more frequent:

  • We made it possible to write a subsection of a legacy section, in modern technology, using a nested router-view used only from new interfaces, helped by the magic of CSS.
  • We also planned for a way to build components, written with new technologies, that could be embedded both in legacy and new sections. We used an embed-like approach, each component has an init method that is invoked by passing the DOM element that includes the component.

this, in a hindsight, was a very good idea since we did find cases that allowed us to develop in a modern way:

The search area is an example of a component that has been used in different sections or different applications

Step #2: change the code base structure

At the beginning of our SPA, we had the nice idea of organizing our source code by dividing the controller from the view. Over time we realize that by adding a simple grouping by section we could improve source navigation and project startup for a new developer.

So we started from the repository organization:

  • host standard Vue project structure to ease the understanding for developers new to the codebase or the company (why we choose VUE);
  • separate the resources by section. Legacy code has been relegated to a specific folder and our objective is to drain code from there (when we refactor) and reach a point where we will have an empty folder: all legacy code will be translated into a new one.

Step #3: switch technology

The experience gathered by working on the POC proved to be precious. We leveraged such experience to write the real loader and run our legacy application as a simple module inside the new loader.

But not everything went smoothly. If we had the chance to rewrite the whole legacy part we could improve performance greatly, instead we kept the same performance as the old code when loading the legacy part. End-users did not benefit from this upgrade.

One of the reasons why we did not feel safe in undertaking a whole rewrite is that not all sections are widely covered by an automated test suite. Not having an automated test-suite opens up just too many chances to miss specific edge cases or usage patterns that might lead to new regressions in the code. This is something we learned over time: tests greatly improve your confidence in refactors because while human memory can be lost, tests are forever.

Conclusions

In the end, adopting modern JS frameworks proved to be a remarkable improvement: developers can stay focused on application logic, and not on low-level details. For each new feature, we now have less code to maintain and better performance for our customers. Legacy code and new code co-exist in a framework independent mix of components that build our SPA.

The new architecture also brought us other improvements:

  • Simpler architecture makes it easy to implement optimizations. It is now much easier to take a big section and split it into smaller chunks, and easier to maintain.
  • Developers never feel like “no one can hear you scream in the space”: modern frameworks mean lively and helpful communities.
  • Less legacy code means less “black boxes”, more code is accessible to a wider developer community, improving the quality and the maintainability.

The next step will be to improve the test suite.

Have you ever had to force the co-existence of legacy code with modern frameworks? How did you choose to do it? Please let us know in the comments below.

--

--