The Architectural Solution for Scaling Projects: Micro Frontends
Do you often find yourself having a single test suite with 500+ tests? What if there was a better way, like ten suits with 50+ tests? When it comes to deploying smaller pieces of code instead of giant chunks, micro frontends are the answer, especially regarding complex web applications.
A while ago, I was dealing with the problem of a single test suite with hundreds of tests. At the time, every deployment felt like a special kind of torment. To illustrate, imagine yourself sitting at your desk on Friday, a few hours before your shift ends, planning a long-overdue deployment to production. You start merging PRs, running tests and preparing mentally for a big finale. Then a moment of silence comes, you hit the deploy button and “bam.” The pipeline starts to fail, everything is red and one of 500+ tests fails. After a moment of silence, you open up the logs to check what happened. While noticing your teammates scratching their heads, you find an imposter: “Expected X found Y.”
Although exaggerated, I am sure most of you have had similar experiences. To change that, consider using micro frontends with your next project.
What Are Micro Frontends?
As defined by Martin Fowler, micro frontend architecture is "an architectural style where independently deliverable frontend applications are composed into a greater whole.". Therefore, your big app can consist of multiple independent smaller apps through a shared container. Each one of these small apps is then a micro frontend. Micro frontends follow container or host architecture where the container serves as a host on which we can install one or more small independent apps.
The theory came to life in November 2016, after microservices architecture's remarkable success in front-end development. At the time, the leading technology consulting firm Thought Works added the term “micro frontends” to their “Technology radar”. A year later, in November 2017, they recommended using single-spa for micro frontends implementation. Finally, in February 2020, Zack Jackson introduced "module federation" as a plugin for Webpack 5.
But enough about theory and history, let’s look at a practical example of how we used micro frontends to make a growing project easier to handle.
The Context and Problem
Firstly, you should know we worked with a single monolith web application to understand our thought process and solution better. It had a user, entity and administrator role. We created it with React and Redux as a state management solution. There were over 2k components filled with complex business logic, lots of unit tests, CSS files, and similar. You get the gist.
As the project grew in size, adding new features was already a burden, let alone working on them. The motivation among the engineers on the team was also slowly fading away. We realized that something needed to be done and that micro frontends held the key to solving our issues.
The First Encounter
The decision to change our architecture and, more importantly, how to change it properly came after long research into the subject.
At first, we thought of isolating each role into its separate entity, its micro frontend. But, as we did this, another problem started appearing. What about shared UI components? Therefore, we ended up isolating them in their package and using rollup to manage the whole thing.
Authentication was next, but since we had our custom authentication service on the back-end, it was easy to split up the functionality to match/serve the micro frontends.
Finally, we had to figure out how to deliver all of this. When it comes to micro frontends, there are two approaches to achieving integration between micro frontends and containers.
Choosing the Right Solution
To be able to select the correct solution to your problem, you should be familiar with both of the approaches: Build-Time integration and Run-Time integration.
- Build-Time integration is widely used today. It refers to a single container that installs all the apps, such as libraries. It is similar to what we do with npm.
There are some pros to this approach. It’s generally easy to implement and it’s a low-effort solution. However, it has its disadvantages too. Mainly what plagues this approach is build issues and colossal bundle sizes. Moreover, containers must have all your dependencies, leading to bundling size issues and deployment issues. As a result, you must redeploy containers whenever a dependency changes in any micro frontend.
- Run-Time integration moves from the concept of containers installing micro frontends to the one where containers are acting as hubs, in which micro frontends are called on-demand.
This approach has three types of compositions:
- Server-Side Composition means that the back-end decides when and which micro frontend to load. For example, you can set up a router on the server and have it route each URL to its micro frontend.
- Edge-Side Composition means you must use a CDN to orchestrate what happens when. A great example is to use AWS CloudFront with Lambda@Edge. This approach will reduce the latency that plagues Server-Side Composition.
- Client-Side Composition means that we will deploy the container separately and leave each micro frontend exposed. Consequently, the container can consume them as needed, like they were packages it can lazy-load when and if required. This approach achieves proper decoupling as each micro frontend is independent of the container.
Returning to our example, we decided to go with Edge-Side Composition. We already had a shared component library, meaning micro frontends only contained business logic. And, to be completely honest, it also seemed easy to implement.
So, after months of hard work, we released the first version. It was a great success:
- performance improved
- user satisfaction grew
- developers happiness increased
Module Federation to the Rescue
Going a couple of months into the future, the client had this great new idea. They suggested splitting one of the core features of the administration panel as a separate SaaS with its brand, authentication and so on. You would think that moving it to its micro frontend and calling it via AWS would work, but sadly no. The feature was tightly coupled with UI and business logic. Therefore, we couldn’t easily separate it into its own entity and had to dig deeper to find a solution.
Quickly, we came across this nifty plugin in Webpack 5 called Module Federation. What this unlocked for us was nothing short of incredible.
Webpack Module Federation allows JavaScript applications to dynamically import code from another application at runtime from different URLs.
Let this sink in.
Are you wondering if this means you can import your code whenever needed without worrying about the bundle size or servers? Yes, it means precisely this and, therefore, unlocks dependency sharing.
For example, we developed our app and feature in React and both use a shared component library. Now webpack will understand that, and it won't import React twice nor import the component library. Most importantly, you can use Suspense and Lazy to provide a fallback if something goes wrong or while loading.
After we could fully separate the core feature from the app, we could theme and customize it as needed and make it a part of any app we wanted.
Not Every App Should Be a Micro Frontend App
As with everything in development, there are always advantages and disadvantages. From the example above, we can learn that micro frontend is not a solution to every problem.
If your app is large to the point that it's becoming hard to manage, or if your future development is going to benefit from it, go ahead, invest some time in it, and you will surely like the result. However, using micro frontends can sometimes bring new and unnecessary challenges.
Here is a short list of the advantages and disadvantages I can identify based on my experience.
Pros:
- scalability
- performance
- maintenance
- testing
- tech stack agnostic
- ability to monetize different modules (SaaS)
Cons:
- learning curve needed before implementation
- time-consuming implementation
- increased cost (AWS) of implementation
- not suitable for small and medium projects