Strangling Legacy Code: Bynder’s Frontend Modernisation Journey

Bogdan Moisa
Bynder Tech
Published in
5 min readDec 19, 2023

TL;DR: We used microfrontends rendered using React Portals to modernise parts of our legacy frontend codebase in a gradual way; a strategy called the strangler fig pattern.

Many developers may have heard or even came in close contact with a part of the application’s code base that is so obscure, so perplexing, so shrouded in mystery that almost no one knows how it works or why it works. It is so revered (and feared) within the organisation that few even dare to look at it, understand it or, God forbid, make changes to it. Yet, this cryptic piece of code powers a critical or even a large part of the application. That is our legacy. So how do we deal with it?

What is Legacy Code?

Old, unmaintainable, complex, outdated, unsupported, hard to debug, are terms associated with legacy code. Code can become legacy if the technology stack it relies on has been surpassed by more modern ones, or if the standards by which it was written have become obsolete.

Anyone who is working on large applications will come across some legacy part, which is, at most, patched from time to time if something breaks. It is also true that in most cases the legacy part is arguably the most important one, handling the basic functionality of the application.

Our Legacy Code

In our case, the parts of the application that needed upgrading handled core functionalities like search and displaying results, uploading and downloading files, filtering and others. Our team focused on the search functionalities, which also involves displaying the results of a search (we call these assets). To bring it up to a modern stack, we had to migrate both backend and frontend code. In this article, I will focus on the frontend part.

How we did it at Bynder

Our frontend was rendered in ColdFusion, a JVM based language, and the UI interactions were handled with jQuery. When we started, we had two choices: either build everything from the ground up and do a big bang release or take it step by step.

A big bang release was considered too risky. It would have taken too much time and who knows, by the time it was released, if it ever was released, might’ve already turned into legacy code.

We decided to take a different approach: the strangler fig pattern. The name references a type of vine that grows alongside a tree. As the plant expands, it consumes the tree, eventually replacing it.

In a similar fashion, our strategy was to peel off the old code little by little, replacing it with a more modern counterpart until the legacy code would be completely gone.

In the frontend, we chose React for our modern solution. The strategy was to develop several microfrontends which, by using React Portals, would replace the old UI. For maximum impact we decided to start with one of the most used and important parts of the application: the asset view.

This page is where our clients start searching for their assets. We started our migration with the part in the green rectangle.

Embedding New Code Into The Legacy Application

The first thing we do is render a div in the legacy application. This will be the mounting point of our microfrontend.

<div id="rootMicrofrontend"></div>

In our React application, we render all these microfrontends based on the condition that the rootId exists.

<IfExistsThenRender selectors={rootMicrofrontendSelector}>
<Microfrontend />
</IfExistsThenRender>

And inside the Microfrontend component, we create a Portal to mount the new React code.

export default function MicrofrontendRender() {
return ReactDOM.createPortal(<Microfrontend />, document.querySelector(roots.rootMicrofrontendSelector));
}

This process enabled us to modernise parts of our application, while also supporting our legacy codebase.

Reading and Emitting Events: Communicating With Legacy

Since the microfronted we just mounted is separate and in a completely different repo from the rest of the application, communication between the two systems remains a problem.

The microfrontend needs data from the legacy code, it needs to react to events that are sent from other parts of the application and in its turn, it has to send events to be picked up.

When it came to passing data to the microfrontend, we leveraged the server sided nature of our legacy application to avoid having to fetch data on initialisation. For example, we pass the entire first page of assets to be rendered as a prop, thus avoiding an extra request to the backend.

Props are passed to the new application by assigning them to the window object.

     window.bynder.propsMicrofrontend = {
firstPage: #firstPageAssets#,
collectionId: "collectionId#",
defaultSortOrderBy: "#orderBy#",
};

Passing props works well for data that is available on render but for dynamic events we had to develop another strategy.

We leveraged Custom Events that can be dispatched from the legacy application or from the microfrontend. When a search action takes place in the legacy code base, we need to update the state in the microfrontend. A custom event for this could be:


var searchAssetsEvent = new CustomEvent("search_event_outside_microfronted", {
detail: { filters: params, callback: finaliseFetchingNewAssets }
});
window.dispatchEvent(searchAssetsEvent);

In some cases, we need extra information and a callback function to execute after we handle the event in the microforontend.

In the React application we have set up event listeners for our custom events so we can handle these actions. We opted for something like this:

export class EventListeners {
private events: { [key: string]: (event: CustomEvent) => unknown };

constructor(callbacks: EventCallbacks) {
this.events = {
'search_event_outside_microfronted': (event: CustomEvent) => {
const filters = event?.detail?.filters;
const jQueryCallback = event?.detail?.callback;

if (!isObjectOrUndefined(filters)) {
throw new Error(
`Filters has to be an object and can't be null. Received ${filters}`,
);
}

if (typeof jQueryCallback !== 'undefined' && typeof jQueryCallback !== 'function') {
throw new Error(`Callback has to be a function. Received ${jQueryCallback}`);
}

callbacks.onSearchAssetsEvent(filters, jQueryCallback);
},
};
}

public listen = () => {
for (const [name, callback] of Object.entries(this.events)) {
window.addEventListener(name, callback as EventListener);
}
};

public stopListening = () => {
for (const [name, callback] of Object.entries(this.events)) {
window.removeEventListener(name, callback as EventListener);
}
};
}

This class is then instantiated in a hook that we can reuse in multiple parts of the frontend application.

Remember: Keep Strangling It!

The strangler fig pattern has its advantages and disadvantages, because as you can see, the migrated code is still coupled with the legacy one and context switching can become a problem sometimes.

However, this strategy allowed us to modernise important parts of the application and opened up development of new features on top of the existing code base.

The most important thing about this approach is to have the organisation fully committed to carry out this initiative until the end. Stopping the modernisation half-way through will double the code that needs to be maintained and may stagger future development. The strangler fig must end up replacing the old application otherwise we will end up with two sick and ugly trees.

--

--