We Frankenstein’ed our monolithic project’s legacy code with React and maybe you should too

Konrad Abe
9 min readMar 1, 2020
Photo by Chris Hall on Unsplash

My team is responsible for one of the oldest codebases still in active use in our company. I mean, it’s not like we’re gathering in a circle of old candles, chanting ancient songs and reciting the laments of past programmers from dusty tomes but as you might know, software projects that reach a certain age can grow a lot and get harder to maintain.

Dealing with the monolithic codebase of a project, which you know will continue to be used and build upon, can be tricky at times. There are important decisions and compromises to make. I will provide you with a few easy to follow steps that might help you regain control and make both management and customer happy.

To give you a rough idea of what we work with, my company is — among other projects — responsible for the development and maintenance of the Product Information/Content Management System (PIM/CMS) for a large German manufacturer in the automotive industry. Nearly all information about the vehicles that you can see on the manufacturer’s website, as well as data used by car dealers, car rental companies, used cars and even their online car configurator, can be created or modified via our PIM in line with market and language requirements.

When I took on my current position as senior frontend architect, I sat silently during our first code browsing sessions, spent hours navigating through the folder structures and when we did pair programming, I swallowed some of the remarks that came to mind when being completely outright would have made me look like an oh-so-wise (or pretentious) jerk.

Lesson 0 | Don’t judge!
If you are new to a project, take your time to understand the team, the history of the project and the environment that led to the current situation.

When the project was started roughly 10 years ago, most of the code was written in clean vanilla javascript against a Java/Spring Boot/Maven backend. For some additional functionality selected jQuery plugins had been employed. At some point, AngularJS had been introduced to the project and new pages and features were written with this framework. As time went by, more and more features and modules had been stacked on top and the PIM grew with each challenge, so that when google did the big rewrite from AngularJS to Angular 2, it had become too expensive at that time to migrate/upgrade the code to Angular 2.

Previously simple classes were extended again and again with each feature request and even though the team had relied on proven best practices of object-oriented programming in many places, the sheer amount of requested features and limited time budgets had led to some very peculiar solutions.

Projects of this size can’t be rewritten on a whim and the daily business often doesn’t leave you enough room to do regular cleaning and refactoring runs. I was fully aware that I was in for a rough ride when I took on this job. Before signing my contract, I spoke to my future project manager, the team and the leads of other teams in the company to make sure that both knowledge and motivation to change things for the better would be available.

Lesson 1 | Talk with people
Try to get a good idea of what people are willing to do, capable of and what constraints might affect the things you are planning to do.

After spending enough time with the code and team, I started making plans. We had enough knowledge of React and Redux in our ranks to take these libraries into consideration and neither of those would severely lock us in when searching for new hires. As the learning curves for both aren’t very steep, it would allow us to keep training budgets for fresh developers manageable.

If your company has preexisting knowledge of other libraries or frameworks such as Angular (current versions) or Vue, React is not the only viable choice but it was our choice.

After getting some time free from my daily tasks, I built two small prototypes of React/Redux modules and implemented them in our old code. The first was the base for a globally present info-center, an information hub the user can slide in from the sidebar to check error and notification messages. The other one was a mock that I placed in one of the many larger .jsp pages to check if both modules would work and could use the same Redux store without causing problems with the existing jQuery and AngularJS code.

Lesson 2 | Make a Prototype ASAP
Don’t just assume and plan, test early! If you think something should work, make a minimal use case prototype and see if your assumptions are correct before proceeding to make plans and calculate budgets.

With those prototypes, some facts about the libraries in question and a rough idea in mind how to proceed, I went to my project lead and explained the plan I had formulated in my mind. We would continue to maintain the old code to the best of our abilities while new features would be written in React where feasible and reasonable. With every larger feature request from the customer, we would communicate the need to make adjustments to the old code, explain why the requested changes would take longer to build than usual and start rewriting old AngularJS features in clean and reusable React code.

Lesson 3 | Come prepared and with a business plan
When you wish to make larger changes to a project, prepare yourself for the discussion with those making the final calls. Do your homework, know what you are talking about and first & foremost, try to find a solution that can be sold to the customer. This usually makes the management happy.

The term we used when we started this project was Strangular. I tried to find the old article again (citation needed) that I had found and which had coined this term for us. Our new and shiny React code would strangle the old AngularJS code and replace it piece by piece until nothing is left. When looking at our project today, I lovingly call it a Frankenstein’s Project. There are bits and pieces of old code and new code side by side and both are running well independently without causing any problems. All it took was one strike of lightning (our initial decision to make this step) and so far neither the team nor the customer came running with torches and pitchforks to my workplace.

[ cue evil laughter ]

Photo by freestocks on Unsplash

These are the steps we took to make this work and keep it scaleable

Separation

Being build as a Java/Spring Boot/Maven project, our code is divided into multiple Java modules containing the backend code as well as a “web-module” with our old frontend code.

We wanted to keep the new code separated as good as possible for a number of reasons. We decided to use webpack as our bundler, have almost all of the new code covered with tests and run the code against a mock API on the webpack dev server without building and running the java code on our tomcat server.

With the help of a backend colleague, we created a new Maven frontend-module that is now tied into our project’s build process and takes care of installing node/npm, bundling our assets and handing them as a webjar to the old web-module. This way there were no additional changes needed to our deployment workflow.

Bridging two worlds

As our project is still mainly AngularJS code living in .jsp files and we needed to be able to initialise React components and modules in many different places in our old code, we had to build a bridge between both worlds.

Every “new world” feature/module we are building is registered with our reactBridge which is available in the scope of the “old world”.

// src/utils/reactBridge.js
import renderInfoCenter from '../components/modules/info-center'
import renderModuleA from '../components/modules/moduleA'
import renderModuleB from '../components/modules/moduleB'
const reactBridge = {
renderInfoCenter,
renderModuleA,
renderModuleB,
}

export default reactBridge

In our old code, we included our reactBridge.js file and built an angular factory for our Services module to make the window.reactBridge available.

// services/reactBridge.js
angular.module('Services').factory('reactBridge', function () {
'use strict';
return window.reactBridge;
});

If we now needed to initialise or render a react module, we could simply place a hook in the code like <div id=”infocenter” /> and call the render function where needed.

// angular-base-layout.jsp
<
script src="${BASE}/webjars/frontend/vendor.js"></script>
<script src="${BASE}/webjars/frontend/base.js"></script>
[...]<script>
window.reactBridge.renderInfoCenter()
</script>

For making Redux dispatches available to old code, this was a bit more complicated but still manageable. We import our store and all dispatchable actions that need to be exposed to the old code for shared functionality and add them to the reduxBridge.

import store from '../redux/store'
import { addNotifications } from '../redux/actions/notifications.actions'
import { dispatchActionForModuleA } from '../redux/actions/moduleA.actions'

const bridgeDispatch = (func, payload) => store.dispatch(func(payload))

const reduxBridge = {
addNotifications: payload => bridgeDispatch(
addNotifications,
payload
),

dispatchActionForModuleA: () => bridgeDispatch(
dispatchActionForModuleA,
payload
),

onStateChanged: (callback) => {
store.subscribe(() => {
const state = store.getState()
callback(state)
})
}
,
}

export default reduxBridge

Again the angular module

// services/reduxBridge.js
angular.module('Services').factory('reduxBridge', function () {
'use strict';
return window.reduxBridge;
});

If we were to dispatch an addNotifications action for the info-center, for example, we could easily do this in the successAction of our API call.

// somewhere in our code...
showInfo : function(message) {
var data = getMessageData(message, 'info');
typeof reduxBridge === 'object'
? reduxBridge.addNotifications([data])
: console.log("reduxBridge not available after timeout")
},

Decide for each Feature Request individually

If I had all the time in the world, I would completely rewrite the old project, bit by bit for every request that the customer makes. In an ideal world, that might be possible. In reality, though, you have to make some tough calls and keep your customer in mind, not only your developers. Finding a healthy balance is key here.

There will be cases when either the deadline demands quick actions or the remaining budget for the quarter is nearly depleted. In those cases, it might be the better choice to keep your internal idealist in check and do what’s best in the short run.

Lesson 4 | Choose your battles wisely!
Find a balance between refactoring, rewrites and adjustments in regards to budget and time and depending on the requirements. In the long run, new and well written code will benefit both customer and the team managing the code but in some cases you just have to get shit done quick. Make wise decisions for the better of the project.

What did we gain?

With all the changes we made and our new React/Redux frontend-module, we’ve made a lot of improvements both for us and the customer. Here’s a short and not exhaustive list:

  • Webpack Bundling with tree shaking
  • Webpack Dev Server for better FE code development
  • modular and reusable react components
  • more velocity with every completed feature
  • new code is easier to test and reason about
  • mock data / API for development independently from BE team
  • Redux store as a single point of truth instead of old AngularJS scope
  • Redux middleware as an abstraction layer between frontend code and API
  • no need for a complete relaunch
  • reduction of technical debt
  • we can pick an attack the most relevant parts of the code when and where we want
  • even more motivated team members than before

What did we lose?

We spent some time on our internal budget to develop a prototype before addressing the customer and starting to solve feature requests with the new setup.

In some cases, we still mainly maintain and extend the old code even though rewriting it would be so much more fun.

This journey was not without its perils. Making huge changes like this to the way you work in a company is likely to create some friction. You need to clearly communicate your estimates when planning new features. You need to include the customer in this decision process if it means that things will take a bit longer while getting started. You need to create a shared understanding with both your management and the customer why building up technical debt will cost a lot more money in the long run.

In the end, I can wholeheartedly say that I never — not even in the slightest — regretted our decision. Happy developers work faster. Clean code is easier to maintain. Clean code makes for happy developers.

Some words about me:

If you want to read more of my articles, feel free to check out my author page or my series on building a React Native Web App with Hooks, Bells, and Whistles with Expo SDK33. I’m also currently working on a new series about building more complex and scalable apps using React/Redux, where I’ll go into details about how and why I do stuff the way I do as well as some articles on my experience in building games for web and mobile with React.

Here are some of my recent topics:
- Building games with React Native [Series]
- Spread & Rest Syntax in Javascript
- clean and simple Redux, explained
- Game Theory behind Incremental Games [Series]
- Custom and flexible UI Frames in React Native
- React Native Web App with Hooks, Bells, and Whistles [Series]

--

--

Konrad Abe

I’m a Web / App Developer & father 👨‍👩‍👧 doing freelance and part-time agency work since 2003, 💻 building stuff on the side 🕹 and attending conferences 🎟