From Old to New — Case Study, Part 3— The Stack

The story of migrating an obsolete web application to a modern stack, progressively

Sefi Ninio
Israeli Tech Radar
7 min readFeb 1, 2023

--

We had a monolith obsolete web application, with an ancient and unmaintained stack, very bad code design and bug fixes that produce either new bugs or regressions. In this series of posts, I’ll describe the migration steps.

In this third installment of the series, I will describe the stack we chose given the challenges we faced, which I went into detail in previous parts: Part 1 and Part 2 — go ahead and read them first if you missed them.

But first — a general rule of thumb as a Tech Lead trying to decide the tech stack and architecture approach for the project.

There is always more than one good way to do it

Don’t make the decisions yourself without consulting the team, and dictating high and mighty facts. Doing so will be crushing their creativity and send the message that you always know better so there is no point in voicing their opinions — which is bad for morale and narrows down the thinking pool and by extension the variety of potential solutions.

Another point to consider is that at the end of the day — they will have to use that stack in their day-to-day, and not being part of the thinking process can cause resentment.

The worst thing a Tech Lead can do is silence the team. From my experience, the most profound “Hhhaaaa” moments I had were when talented but less experienced team members came up with an original approach or a library I knew nothing about that really saved the day.

We all have blind spots

So, the obvious way to gain team members’ trust is by encouraging them to be part of the process and decision-making — in an honest and open-minded discussion.

  • Ask for their opinion — what alternatives do they suggest
    (for example — should we use a state management solution? If so — which one?)
  • If you are not convinced the proposed approach aligns with the requirements — ask the team member that proposed it to implement a short POC. If it is successful and the team is convinced — great! if not — what are the alternatives and should we try another POC? Do we have the time for it?
    (for example — using AGGrid, or Module Federation vs iFrames for Micro Frontend)
  • If it is not an important decision, let them have it without a fight — choose your battles — it will earn you credit for the really important decisions.
  • Bottom line — the decision is yours to make, even if unpopular. However, since all alternatives were seriously and openly discussed and genuinely considered, by that point all team members should be on board with the decision even if they don’t like it.

The Architecture Decisions and Tech Stack

Micro Frontend

Since we had to migrate the obsolete monolith application gradually while allowing users to continue working with the application, Micro Frontend was the obvious choice to make. besides bringing to the table options like continuous deployment of small parts instead of having to redeploy the entire application on every small change, it creates opportunities for siloed team responsibilities and small, easily maintained, focused mini-applications. Every reason that promotes Micro Services in the backend is a valid point for Micro Frontends, with the exception of multiple frameworks, as it promotes Micro Frontend Anarchy.

We had to choose between using iFrames or Webpack5 Module Federation. Eventually, we decided to use both.

  • The old code not yet migrated will be hosted in an iFrame, since the old application was using an old version of Webpack, and updating it was too big of an effort.
  • The new code would leverage Module Federation. Code would migrate gradually step by step from old to new.

This required the development of a robust communication layer that supported shared state, routes sync, and handshake process between iFrames, the module federation remotes, and the host application —
I will go into details in a separate post later on in this series.

Monorepo

We opted for using NRWL/NX monorepo since it makes reusing code and running scripts simultaneously very easy as well as making module federation remotes and hosts configuration a breeze.

Prefer Composition over Configuration

When it comes to component implementation — in the configuration approach, the component expects a large number of props that will change the way it behaves and renders. This leads to components that are less obvious to use and the internal code is often filled with if/else clauses to cover all the cases. It also makes those components very hard to extend as time passes, since a single new prop might break all others and create new edge cases to handle.

This is why we preferred the composition approach — each component expects a minimal set of properties and is very specific — either it is composed of partial components or it is a variant of the component, explicitly defined and wraps the more generic one and modifies it.

The downside of this approach is a larger amount of files and components, but the upside is much simpler, explicit components that are much easier to read and maintain or extend.

Define a robust design system using Styled Components

This includes the very basics like a color palette and the semantic colors layer on top of it and allows support for changing themes on the fly — components use the semantic primary color, which might be different palette colors in different themes.

It also includes things like corner radius, spacing, font size and weight, z-order levels, shadows, etc.

Next, we defined a layer of reusable mixins — common patterns like an all-centered flex container.

This was easily achieved with Styled Components but that is not the only benefit — Styled Components (and CSS in JS in general) allows you to easily pass transient properties to the style to change the way it behaves based on the component state!

Component Library

A component library provides implementations for the atoms and basic molecules that are used to build the product design. We used Chakra and implemented the component library on top to apply the UI/UX as reusable components to be used in the features.

Storybook

Storybook is an important tool for developers and designers alike. It allows developers to implement components in a sandbox without having to bootstrap the entire application which is a huge time saver.

But it doesn’t end there — if you deploy the storybook to an accessible URL, designers can review and verify the implementation aligns with the design and raise issues when it is not — so the feedback loop is much shorter.

State Management

When using the Micro Frontend architecture, each remote application should be very focused and dedicated to a very specific section of the whole, responsible for fetching its’ own data and managing its own internal state. If it is not — you are doing it wrong.

Since the state is minimal, we decided on using Context API. But — Context is notorious for causing redundant renders when it changes. For that reason, we used the use-context-selector library that adds an extra layer of usability as it allows control on when a component reacts to context change and should re-render or not. This is a safe bet since this library is scheduled to be integrated with React core in v19.

I18N

Internationalization is one of those things that is a very big effort to introduce to a mature codebase, so it is very recommended to incorporate it early on. We used i18next and react-i18next — it is a robust and feature-rich solution, that supports interpolation, formatting, plurals, and most importantly for Micro Frontends architecture — namespaces.

Since we didn't want to rebuild and deploy applications every time a new translation key was added or modified, the translation files could not be part of the code.

We defined a namespace for each remote application and the language JSON files were hosted on an S3 bucket by language/namespace. The code fetched them on demand and this allowed changing translation copies without any code changes.

Unit Tests

This is another example of something that is very hard to introduce late — we used Jest and React Testing Library, using snapshot testing where possible and only testing component behavior and not internal logic.

It is very easy to go overboard with testing — for us, code coverage wasn't a goal as it often gets to a point when writing tests takes more than the actual logic they test — it becomes counterproductive and usually promotes fragile tests that often break and must be maintained.

Rely on third-party services when it makes sense

Things like feature flags, logging, and audits made no sense to implement in-house, so we used 3rd party services for those.

Photo by Huma Kabakci on Unsplash

In the next parts, I will go into the details of each of the bullets above and we will finally see some code!

--

--