At Mintel, we have global engineering teams building web applications for our clients to access our global market research, data and insights. As we’ve scaled our engineering teams we’ve faced challenges in ensuring consistency across our applications and to help solve these problems we’ve adopted a micro-frontend architecture. Here, I describe how we implemented this architecture and encouraged adoption across our engineering department.
The problem
At Mintel, we have a large collection of web applications that our clients use to access our global market research, data and insights. Over the years, we’ve seen issues with inconsistencies and siloes emerging between our applications. This led us to prioritise our users’ experience so that it feels effortless and using our applications feels like using a single integrated platform.
Our oldest applications were developed nearly two decades ago, and in that time we’ve seen software development technology change massively— making it challenging to bring these applications together in an integrated way. We’re also rapidly building new applications that better meet the needs of specific segments of our clients and need to ensure these fit into our platform. A monolith approach has limited how quickly we can scale, and completely independent applications have led to inconsistencies and a lack of cross-application integration.
To help solve these challenges we adopted a micro-frontend architecture for our web applications.
Micro-frontends
Micro-frontends bring the microservices pattern to frontend web applications and can provide a massive advantage in allowing independent, autonomous development teams to work on parts of a larger product. Cam Jackson defines micro-frontends as “An architectural style where independently deliverable frontend applications are composed into a greater whole.” This approach has been successfully adopted by many companies such as Ikea, Spotify and SoundCloud.
Independent web applications are built and maintained by independent software development teams. They are combined into a single production environment application using a common deployment pipeline set-up and shell application.
At Mintel, our Core Frontend team provides common frontend libraries and support to our application teams. We decided we could best support enabling our application development teams by implementing a micro-frontend environment to which they could deploy their applications.
Planning
We set out to build an MVP micro-frontend environment within a six-week development cycle, focusing on the key requirements that would give us a system to which teams could begin onboarding.
We identified core requirements and components of the work:
- Simple to adopt with great documentation
- A ‘shell’ application that would compose deployed micro-frontends into a single application
- A shared deployment pipeline template that teams could use to deploy their micro-frontends
- Handle basic platform-wide concerns within our shell application (user authentication, rendering universal components and metric tracking utilities)
Although we could already see exciting potential to take this concept further, we purposely limited the features within our first iteration to ensure we delivered something that worked well enough for teams to use, get feedback and iterate.
Selecting a framework
Setting out, we reviewed existing frameworks that we could use to implement our environment. We looked at Bit, Piral and single-spa before deciding to use Webpack 5 and Module Federation.
There were a few key factors that led us here:
- Webpack was already in use across all of our development teams, which lowered the barrier for adoption.
- It’s easy to set up, doesn’t try to do too much and gives us the ability to dynamically load the JavaScript for each application while allowing us to have control of the rest of our workflow.
- The documentation is somewhat sparse, but the Practical Module Federation e-book helped fill in the gaps.
Deployment
We wanted to keep things simple and embrace a Jamstack approach requiring limited infrastructure. At Mintel, we use AWS for cloud deployments, so we worked with our Cloud Platform Team to set up an S3 bucket and CloudFront for our QA and Production environments. We went with a simple directory structure where each micro-frontend would be deployed to its own directory within the bucket.
To ensure this deployment process was something that teams didn’t need to worry about, and that we were able to make cross-application changes to later if needed, we wrote a GitLab CI template that each application could include. A deployment step would trigger after successful pipeline runs on the main branch using rclone to push the built application to the correct directory in the QA S3 bucket. Releases would be promoted to production by pushing a tag to the main branch.
Shell application and handling platform concerns
Our shell application would be responsible for composing all deployed applications and handling common concerns, including user authentication, rendering common components and initiating metric collection utilities.
Configuring Module Federation
The shell itself is a React application, deployed in the same way that the application development teams would be building. Our Webpack configuration would initialise Module Federation:
- Remotes: We specify where we should be loading our micro-frontend applications from. These URLs point to the S3 bucket directory where each micro-frontend is deployed.
- Shared: The shared option allows us to define common dependencies to pre-load and make available for micro-frontends to use. Here we include React libraries and our design system libraries commonly used across all of our applications to avoid duplication.
Our React shell application handles routing and rendering each team’s micro-frontend React application. Each micro-frontend is imported and defined in a configuration file:
Our shell application handles routing to each of these applications. Using React.lazy allows us to only load the bundle for each micro-frontend when it is rendered.
Authentication
At Mintel, we use OAuth2 to handle user authentication. We decided to use react-oidc-context to do the heavy lifting and handle the authentication flow. With some basic configuration, we were able to integrate with our OAuth2 provider and ensure our users would be authenticated when using our applications.
Rendering universal components
We also wanted our shell application to be the single place our universal components would be rendered. This included our universal header that is used across our products.
With our previous approach of separate applications, we faced coordination difficulties when we needed every development team to update their version of the header due to a new product launch or feature change. By handing this responsibility to our shell application, we’d be able to have changes reflected across all of our products with a single release of the shell.
In order to give individual applications control to customise the header, we passed a callback function to each rendered micro-frontend app as a prop. This allowed us to support individual applications passing props to the universal header while not needing to render it.
Initiate metric tracking utilities
We also needed to ensure our teams could get the metrics they need to debug issues and our product managers could understand how our clients are using our products. We added initiation for Sentry and New Relic within the shell, so our development teams could get access to error and performance metrics. We use Mixpanel to track how our clients use our products, and embedded our API for logging events in the shell, providing each application with the ability to log interaction events and giving us the option to easily make platform-wide event changes in the future.
Adoption
We’ve seen great adoption of our micro-frontend approach with positive feedback from the development teams. With well-documented set-up and usage guides, handling common concerns such as metrics and deployment and encouraging separation of frontend applications from backend services, our development teams have been able to build and deploy new applications faster. We’re now at the point where teams can bootstrap a new frontend application and have it deployed in a production environment in less than a day’s work.
Our next challenges are continuing to build new features to support our application development and figuring out a broader strategy for moving all of our existing applications over to this new architecture.