How To Convert a Huge Frontend Monolith to a Micro-frontend

Published on

When starting a new project, most technology teams begin with a single frontend repository. This worked very well for us at Dream11 too, when our tech team was small in the early stages of projects. But as the team grew, maintaining our content management system (CMS) became more and more difficult. With a growing team, making work processes as seamless as possible to deliver high performance was a priority, so we identified problematic bottlenecks and reinvented our way of work when it came to our CMS application.

What were the bottlenecks?

We found that:

  • Shared code led to too many conflicts.
  • Continuous integration and deployment (CI/CD) became cumbersome, overloaded, and inefficient.
  • Experimentation, migration, and refactor became difficult.

These problems were already tackled in an architecture called microservices, vastly used in back-end architecture design. ‘Microservices’ mean dividing a monolithic application into smaller, independent services. On similar lines, Thoughtworks defines micro-frontend as “An architectural style where independently deliverable frontend applications are composed into a greater whole.”

What was our resolution?

Work targets are easier to achieve when they are divided into smaller milestones. Similarly, we realized that when a product expands rapidly, it is better to divide the team into smaller verticals. Each vertical could then focus on a specific target- just like an e-commerce application, for example, where there could be separate verticals for offers, user personalization, and checkout.

We adapted a micro-frontend architecture that helped solve multiple problems. It split scary monoliths into smaller independent applications, which could then be tested, deployed, and maintained in a focused scope. This helped independent vertical teams deliver quicker without worrying about technical debts and heavy regressions, thus, amplifying the overall performance of deliveries.

There are multiple ways to go about implementing a micro-frontend architecture.

  • Handle it at the infrastructure layer — Configure your infrastructure to serve multiple applications under the same domain.
  • Handle it at the component level — Deploy your components on delivery networks such as Bit, and use them over the network or burn them into the package during build time
  • Handle it at the run time — You have a controller script/app running on the browser which selects which script/app to load.

We identified that we could logically separate our flows easily based on routes and thus after evaluating the options, handling at the run time was the right fit for us. It enabled us to have -

  • A micro control on switching the context of applications based on business logic.
  • Authentication, authorization, and layouts were implemented once.

High Level Architecture

Relating to a microservice analogy, we had multiple independent codebases. In order to serve them as one web application, we implemented a container application that stitched together all the relevant codebases.

The container application conceptually should know when to switch applications and then bootstrap them into the current runtime. How to decide, can be based on business logic or your application. For our use case, the business flows had very less intersections, so we went with loading different applications simply based on routes.

The crucial part was bootstrapping individual applications at runtime. Every web application had an entry point where the root component resided. The container application needed access to this entry point component. The child applications made the root component available to the container by hosting it in a global context.

But how did we implement the architecture? For this, we had to answer two questions for our container -

  • When to render a child application? — We grouped our applications under the router param. For example — /App-A/* would serve all routes under App-A.
  • How to render a child application? — In all our applications, we made the assets and their URLs available using a JSON configuration, along with the entry script to execute to launch a child application.

Though we used React for all our rendering needs and webpack for bundling, the concept could be applied to any other frontend frameworks.

Low Level Design

Now let’s get our hands dirty with some implementation details. Let’s set this up in our local systems. In the above architecture, there are two types of application:

  1. Container Application

As mentioned earlier, this application needs to know when and how the child application loads. To achieve this, it needs -

  • To load a specific application’s bundles based on routes
  • To forward asset requests to the specific child application.

2. Child Applications

  • These applications are hosted independently on different domains
  • The child applications need to make it possible for the container to load them This involves adding scripts to mount methods on the window object on the entry point of an application.

Consider that three applications were running on the following ports on the local system:

  1. http://localhost:2000 — Container Application
  2. http://localhost:3000 — App-A
  3. http://localhost:4000 — App-B

In order to bootstrap a child application, the container needed to load all the assets required for the child application to start working on the browser. The information about such assets needed to be provided by each child application. Webpack, being our go-to bundler, and webpack-asset-manifest plugin used with webpack, produced a manifest file containing assets information as follows:

Let’s see how the container application structure works. The container application code comprised mainly of three files.

  1. MicroFrontend.jsx

The above code is very much similar to what is recommended by Cam Jackson. As the name suggests, the purpose of this code was to load the application based on the host and name provided. It made an ajax request for asset-manifest.json and created a script element in the dom. Once the script was loaded, there were two functions added by the child application into its code, i.e renderAppA, and unmountAppA.

  1. ContainerRoute.jsx

The above code consists of routing information for child application. /App-A/* forwarded all routes to the ’s router.

  1. server.js

The child application may have dynamic chunking implemented in it. When a child application was served from a Container Application, all requests for chunks were to go to the same URL, but instead, they were forwarded to the child application’s URL i.e /App-A/ . To solve this problem, we found and forwarded requests to a specific server. Below is the code that was used.

In this code, we forwarded all .js file requests consisting of App-A in it’s URL, to App-A.

Along with these three parts, the container app may consist of some common screens. We ended up creating a lightweight login and home page, which were shared by all child applications.

Now let’s discuss how we addressed the second part of the implementation, that is, children applications. We will take an example of one child app created with create-react-app.

Following were the changes needed in this application

  1. index.jsx — attach bootstrapping functions to mount and unmount application

In this code, we created two functions for mounting and unmounting child applications into a container application.

  1. server.js — for allowing cross-origin request based

We allowed cross-domain requests coming from container applications. For security purposes, a list of domains was maintained so that other domain excluding the list could be easily stopped

That’s it! We are all set to convert any existing application into the micro frontend.

The Pros

  • No tech debt was carried: Since all applications were isolated, any breaking change in one wouldn’t affect other applications
  • Room for experimentation: One could try different applications stacked together
  • Concentrated QA Effort: In the case of regression testing, there was less area to cover.

The cons

  • There was a slight impact on the development experience as we had to run multiple applications
  • There was less code sharing between applications since the stack of each application was different

Overall, the benefits of micro-frontend implementation were remarkable and for us, they outweigh the drawbacks.

References:

Related Blogs

Here’s how Dream11’s Director of Engineering strives to provide world-class user experiences
In our #BeyondTheAlgorithm series, we dive into the heart of the action to capture the success journeys of our Dreamsters. In our second edition, we capture our Director of Engineering - Vinita Miranda's inspiring journey from a Java Developer to a Mobile App expert at Dream11.
October 3, 2023
Turf- Designing the rules of play!
One of the most common traits of a successful application is the consistency in the use of colours, shapes, and patterns that leads to brand identification. At Dream11, we aim to standardise our entire design practices and design components. Turf, our design system, ensures that all our 120+ million users experience the same brand value, whichever platform they may be on.
November 23, 2021
Lessons learned from running GraphQL at scale
At Dream11, we have experienced tremendous growth from just 300,000 users in 2015 to over 110 million users at present. To grow at this blazingly fast pace, we moved to a microservice architecture for developing backend systems.
September 10, 2021