As a frontend developer, all these years you were developing monoliths, even though you already knew it was a bad practice. You divided your code into components, used require or import and defined npm packages in your package.json or mounted sub git repositories into your project, yet you ended up building a monolith. It’s time to change it.
All frontend applications are a monolithic application in nature, except apps that already implemented micro frontends. The reason is if you are developing with the React library and if you have two teams both should be using the same React library and both teams should be in sync on deployments and always will be conflicting during code merges. They are not separated completely and most probably they are maintaining the same repository and have the same build system. The exit from a monolithic app is formulated as microservices. But it’s for backend! 😱
In general and the most simplistic explanation for microservices is, it is a development technique which allows developers to do independent deployments for different parts of the platform without harming other parts. The capability of independent deployment allows them to build isolated or loosely coupled services. To put this architecture on a more stable base there are some sets of rules to follow which can be summarized as follows: Each service should have only one task and it should be small. So the team who is responsible for this service should be small. About the size of the team and the project, one of the coolest explanation on the internet has been done by James Lewis and Martin Fowler as below:
In our conversations with microservice practitioners, we see a range of sizes of services. The largest sizes reported follow Amazon’s notion of the Two Pizza Team (i.e. the whole team can be fed by two pizzas), meaning no more than a dozen people. On the smaller size scale we’ve seen setups where a team of half-a-dozen would support half-a-dozen services.
I created a simple sketch to give a visual explanation for monolith and microservices:
As you can understand from the drawing above each service in microservices is a standalone application except UI. UI is still in one piece! When all services handled by one team and while the company is scaling up, the Frontend team will start struggling and won’t be able to keep up with it and this is the bottleneck of this architecture.
Additional to its bottleneck, this architecture will result in some organizational problems too. Assume that the company is growing and will adopt agile development methodologies which require cross-functional small teams. On this common example, naturally, product owners will start to define the stories as frontend and backend tasks and the cross-functional team will never be a real cross-functional unit. It will be a shallow bubble which looks like an agile team but it will be separated deep inside. More on that managing this kind of team will be really a nail-biting duty. On each planning, there would be a question if there were enough frontend task or were there enough backend tasks in the sprint. To address all the problems described here and numerous others, a couple of years ago the micro frontends idea has emerged and it started to gain popularity very quickly.
The solution is actually quite obvious, embrace the same principles which are working for backend services for many years: Divide the frontend monolith into small UI fragments. But UI is not quite similar to services, it is the interface between the end user and the product, it should be consistent and seamless. Even more, in the era of Single Page Applications, the whole application is running on the browser on the client side. They are not simple HTML files anymore, instead, they are sophisticated pieces of software reaching really complex levels. Now I feel like a definition of the micro frontend is necessary:
The idea behind Micro Frontends is to think about a website or web app as a composition of features which are owned by independent teams. Each team has a distinct area of business or mission it cares about and specialises in. A team is cross functional and develops its features end-to-end , from database to user interface. (micro-fontend.org)
From my experience so far, for many companies, it is really hard to directly adopt the architecture proposed above. Lots of others have a huge legacy burden which is nailing them down from migrating to a new architecture. For that reason a softer midway solution which is more flexible to allow easy adoption and secure migration is vital. After overviewing the architecture in more detail I will try to provide some insight into an architecture which is confirming the proposal above and allowing more flexible ways to follow. Before diving into the details, I need to build up some terminology.
Let’s imagine we are dividing the monolithic app structure vertically through business functionalities. We will end up with several smaller applications which has the same structure with the monolithic application. But if we add a special app on top of all these small monolithic apps, users will communicate with this new app and it will compose the old monolithic UI from each small app into one. This new layer can be named as stitching layer because it gets the produced UI parts from each microservice and combines into one seamless UI for the end user and this would be the most straight forward implementation of a micro frontend 🤩
For a better understanding, I will refer to each small monolithic app as micro-app since they all are standalone apps and not microservices only, they all have UI parts and each represents an end-to-end business functionality.
As it’s already known, today’s frontend ecosystem is highly versatile and can be extremely complex. So this kind of straightforward solutions will not be sufficient enough when the time comes to implement it for a real product.
While this article was just an idea, I started a Reddit thread to discuss the idea. Thanks to the community and their responses I can list some problems to be addressed and I will try to describe them one by one.
How to create a seamless and consistent UI experience when we have a totally independent standalone micro-apps ?
Well, there is no silver bullet answer to this question but one of the ideas is creating a shared UI library which is a standalone micro-app itself too. By that way, all the other micro-apps will depend on that shared UI library micro-app. In that case, we just created a shared dependency and we killed the idea of standalone micro-apps.
Another idea can be sharing CSS custom variables on the :root level. The advantage of this solution would be the global configurable theme between apps.
Or we may simply share some SASS variables and mixins between app teams. The downside of this approach would be the repetitive implementation of UI elements and the integrity of the designs of similar elements should be checked and validated always for all the micro-apps.
How do we make sure that one team is not overriding the CSS written by another team?
One solution is CSS scoping via CSS selector names which are carefully selected by the micro-app name. By putting this scoping task to the stitching layer will reduce the development overhead but will increase the responsibility of the stitching layer.
Another solution can be forcing each micro-app to be a custom web component. The advantage of this solution is the scoping done by the browser, but it comes with a price: it is nearly impossible to do server-side rendering with shadow DOM. Additionally, there is no 100% browser support for custom elements yet especially if you have to support IE.
How should we share the global information between micro-apps?
This question points out to one of the most concerned issues on this topic, but the solution is pretty easy: HTML 5 has pretty powerful functionalities which are not well known by the majority of frontend developers. For example, custom events are one of them and it is the solution for sharing information within the micro-apps.
Alternatively any shared pub-sub implementation or T39 observable implementation can do the trick. If we want a more sophisticated global state handler we can implement a shared miniature Redux, by that way we can achieve more reactive architecture.
If all micro-apps are standalone apps, how do we do client-side routing?
This problem is up to each implementation by design. All major modern frameworks are providing powerful routing mechanisms on the client side by using browser history state. The problem is which application is responsible for the routing and when.
My current pragmatic approach is creating a shared client router which is responsible only from the top level routes and the rest belongs to the respective micro-app. Let’s say we have a /content/:id route definition. The shared router will resolve /content part and the resolved route will be passed into ContentMicroApp. ContentMicroApp is a standalone server and it will be called with /:id only.
We must have the server-side rendering for sure but is it possible with micro-frontends?
Server-side rendering is a tricky problem. If you are considering iframes to stitch the micro-apps then forget about server-side rendering. Similarly, web components for stitching task are not powerful than iframes. But if each micro-app is able to render its content on the server side then the stitching layer will be responsible only for concatenating the HTML fragments on the server side.
Integration with a legacy environment is vital! But how?
To integrate a legacy system, I would like to describe the strategy of my own that I named as “gradual invasion”.
First, we have to implement the stitching layer and it should have a functionality of transparent proxy. Then we can define the legacy system as a micro-app by declaring a wildcard route to it: LegacyMicroApp. So all the traffic will hit the stitching layer and will be proxied to the legacy system transparently since we don’t have any other micro-apps yet.
Next step will be our first gradual invasion movement: We will take a small bite from the LegacyMicroApp by deleting the main navigation and replacing it with a dependency. This dependency will be a micro-app implemented with a shiny new technology: NavigationMicroApp.
Now the stitching layer will resolve each route as LegacyMicroApp and it will resolve the dependency as NavigationMicroApp and serve them by concatenating these two.
Then the next bite will come for the footer by following the same pattern with the main navigation.
And then we will continue taking similar small bites from LegacyMicroApp until nothing left from it.
How to orchestrate the client side so we won’t need to reload the page each time?
Well, the stitching layer solves the problems on the server side but not on the client side. On the client side, after loading already glued fragments as a seamless HTML, we don’t need to load all the parts each time on URL change. Therefore we have to have some mechanism which loads fragments asynchronously. But the problem is, these fragments may have some dependencies and these dependencies need to be resolved on the client-side. That means a micro frontend solution should provide a mechanism to load micro-apps and also some mechanism for dependency injection.
According to the questions and possible solutions above, I can summarize everything under the following topics:
- Isolation of micro-apps
- App to app communication
- Consistency between micro-app UIs
- Server-side rendering
- Dependency management
So, it worthed the wait all along this article! The basic elements and requirements of a micro frontends architecture finally started to reveal itself!
With the guidance of these requirements and concerns, I started to develop a solution which I named as microfe. 😎 Here I will describe the architectural goal of this project by underlining its main components in an abstract manner.
It is easy to start with client-side and it has three separate backbone-structures: AppsManager, Loader, Router and one extra MicroAppStore.
AppsManager is the core of client-side micro-app orchestration. The main functionality of AppsManager is to create the dependency tree. When all of the dependencies of a micro-app are resolved, it instantiates the micro-app.
Another important part of client-side micro-app orchestration is the Loader. The responsibility of the loader is fetching the unresolved micro-apps from the server-side.
To solve client-side routing I introduced the Router into microfe. Unlike the common client-side routers, the microfe router has limited functionalities, It does not resolve the pages but micro-apps. Let’s say we have an URL /content/detail/13 and a ContentMicroApp. In that case, the microfe router will resolve the URL up to /content/* and it will call ContentMicroApp /detail/13 URL part.
To solve micro-app to micro-app client-side communication I introduced MicroAppStore into microfe. It has the similar functionalities of Redux library with a difference: It is resilient to asynchronous data structure changes and reducer declarations.
The server-side part can be a little bit more complicated in implementation but simpler in structure. It consists of only two main part StitchingServer and lots of MicroAppServer.
Bare minimum functionality of a MicroAppServer can be summarized as init and serve.
While a MicroAppServer booting up first thing it should do is calling SticthingServer register endpoint with a micro-app declaration which defines the micro-app dependencies, type, and URL schema of MicroAppServer. I think there is no need to mention about serve functionality since there is nothing special about it.
StitchingServer provides a register endpoint for MicroAppServers. When a MicroAppServer registers itself to StichingServer, StichingServer records the declaration of the MicroAppServer.
Later the StitchingServer uses the declaration to resolve the MicroAppServers from the requested URL.
After resolving a MicroAppServer and all of its dependencies, all relative paths in CSS, JS and HTML will be prefixed with related MicroAppServer public URL. One additional step is prefixing the CSS selectors with a unique identifier of MicroAppServer to prevent collision between micro-apps on client-side.
Then the main responsibility of StitchingServer comes into the scene: composing and returning a seamless HTML page from all collected parts.
Even before it was called micro frontends by 2016, lots of big companies were trying to solve similar problems such as Facebook with its BigPipe. Nowadays the idea is gaining momentum. The companies with different sizes are interested in the subject and investing time and money on it. For instance, Zalando open-sourced its solution which is called Project Mosaic. I can say that microfe and Project Mosaic are following similar approaches with some vital differences. While microfe embraces full decentralized route definitions to empower more independence for each micro-app, Project Mosaic prefers centralized route definition and layout definitions for each route. By that way, Project Mosaic allows easy A/B testing and dynamic Layout generation on the fly.
There are some other approaches to the subject such as using iframes as stitching layer which is obviously not on the server-side but on the client-side. This is a very simple solution which does not require so much server structure and DevOps involvement. The job can be done by the frontend team only, so it creates a less organizational burden on the company and also it is less costly.
There is a framework already out there called single-spa. The project relies on naming conventions of each app to resolve and load micro-apps. Easy to grasp the idea and follow the patterns. So it can be a good initial introduction for experimenting the idea on your own local environment. But the downside of the project is you have to build each micro-app in a specific way so they can play nice with the framework.
I believe that micro frontends topic will be discussed more frequently in time. If the topic manages to get the attention of more and more companies, it will be the defacto way of development in large teams. It can be really beneficial in the close future for any frontend developer to grasp some insights and experience on this architecture.
I’m heavily experimenting on micro frontends with a noble goal in my mind: Creating a micro frontend framework which can solve the majority of problems without compromising of the performance and ease of development and testability. If you have any bright ideas to show, do not hesitate to visit my repositories, open an issue or reach me out via comments below or Twitter DM. I will be there to help you! 🙂