Prasanna Venkatesan is a Lead Consultant with ThoughtWorks, and an Archimydes fellow. He is a full-stack web developer with experience in building and leading development teams that build high-performance enterprise solutions for some of the world's leading organizations and brands. Prasanna is also a regular open-source contributor, having worked widely in repositories like mountebank, RapidFTR, and OpenMRS. Most of his open-source contributions are from building utilities (both mobile and web).
You can also view this talk here.
Modern web applications tend to have complex and feature-heavy Frontends when compared to backends.
With so many choices for frameworks and programming paradigms, building a consistent Frontend to scale is a challenging problem to solve. We cover ways in which you can scale your Frontend application and your development teams by using a Micro-Frontends design pattern.
I'll start by introducing the pattern of Micro-frontends first. Then we'll be looking into some of the key decisions that need to be taken while starting a Micro-frontend project.
Finally, we will see the circumstances where this pattern will be effective.
In general, scaling applications implies scaling backend applications to serve an increasing number of users. Usually, it's about how to:
All these parameters are typically applicable for the backend applications.
For frontend applications, we typically stop with a good CDN to deliver static assets efficiently. However,
Scaling frontend apps is also about scaling development teams, both by size (growing a size of a single team) or count (multiple teams)
Additionally, applications are getting more frontend heavy because:
As a result of this, product teams need to figure out an efficient way to build and deliver frontend applications with multiple development teams working at scale. Product teams need to execute this while reducing bottlenecks in the development process.
Monoliths are not a bad design choice It's always best to start any application as a monolith. Upfront slicing of module boundaries is very hard and tends to go wrong. As the application grows, it's better to identify module boundaries and split them up.
From monoliths, the best choice to evolve the backend services as microservices. We can then guarantee:
However, most of the microservices I have seen are as follows
Independent deployments ! == Independent releases
Teams are able to develop and deploy backends independently. However, they need to wait for the frontend to be developed and deployed.
All of Microservices' promises + Independent releases
Micro-frontends are nothing but taking the concept of micro-services to the frontend. Slice the frontend of the application to respect the module boundaries of the backend, and create an end-end independent release path.
Let's go over some of the major decisions that we need to take during the early stages of a micro-frontend application. I will try to cover the solution(s) that we took while building an application with distributed teams across 3 regions for 2 years. The decisions can vary based on the project context but nevertheless these problems need to be solved.
In order to explain the challenges and decision, I will take up the following use-case — Building an application to allow user to configure and buy a laptop. Similar to that of Apple's.
A user can configure a laptop with various components, accessories, protection plans, etc. The user should be able to search for accessories, or maybe built-in models, and then finally should be able to order the product and get it fulfilled.
Apart from the 3 services - configure, search, and order, I will have another service called "Frame" merely to hold the application together.
Frame: A business logic agnostic orchestrator service that knows how to download the rest of the services' frontend
End users don't care about the tech used. Their experience should not be affected due to tech.
Composing multiple frontends into a single application is one of the first problems that needs solving when choosing micro-frontends.
We can achieve this composition in 2 ways, let's go over the pros and cons of these approaches.
Build-time composition is where multiple frontend applications are built as a single big application and served. This can be accomplished using npm or yarn packages.
A Runtime composition is where the frontends get integrated into the browser directly when the user requests a page. This may be done on the "Server-Side" or on the "Client-Side".
We chose Runtime composition for the project we worked on. Since our app was rendered on the client-side, it was simpler for us to achieve this.
Multiple frontends need to share data with each other. Though this needs to be minimal, it's unavoidable. A couple of options to achieve this is by:
State management tools
A global store in the application and all frontends using the same library to access the store.
Another approach could be to utilize the window (DOMs) eventing capability. Below is a sample event.
We communicated through common redux store and redux events since all the apps in our micro-frontends were using Redux.
One of the hardest problem to solve for is design consistency. In our team, we addressed this challenge by forming guilds. Consider that there are three teams, and each team has a designer assigned to it.
We formed a guild comprising of all designers and some interested developers. They encompass a virtual team. They take all the design decisions and make sure their respective teams are abiding by the central design tenets.
Initially, the guild created a style guide for the application. Mainly CSS and the application teams copy-pasted it from the style guide to build components.
As we developed more features, we started pulling out Higher-order JS components and made them sharable. This is more of an evolution and works well once you have a stable design system in place.
And also, since the teams were using the same frontend framework (React) it was easier for us to build this component library.
Deciding on "How to test" is important. Since it's a relatively newer paradigm and there are lots of moving parts in the application.
Primarily we will be discussing the "Integration tests" and "Functional tests" from the testing strategy, as there won't be much difference in the way the "Unit tests" are done.
Having a lightweight "Consumer Driven Contracts" (CDC) helped us a lot.
A CDC is where the consumer services' give some tests to the provider service. A provider has to run all of its consumer services before publishing an artifact for deployment.
This doesn't need to be very complex and can be done quickly using some lightweight options without using any big frameworks. But then, it's all case by case.
In our scenario, Frame was the consumer of all the services and it shared a simple JSON contract and a small JS test with all of its providers. This ensured that the frame wasn't broken when a service deployed automatically.
This is one of my least favorite testing methods, however, like everything else in tech, it does have some staunch supporters and followers. In our case, we only had a few critical and successful user journeys automated using Selenium for functional testing.
These journeys cut across multiple services and hence are harder to develop and maintain. Some of the FAQs I usually get on these tests are
Ans. The product team and business analysts. They define the scenarios for automation.
Ans. Guild containing QAs from all teams and a few developers.
Ans. Team which breaks it.
Micro frontends are not for everyone. It adds significant overheads with development and maintenance. Here are some scenarios in which you can explore:
A. Distributed self-contained teams, with a need for parallelization
If your development teams aren't co-located, and there is a decent amount of parallelization that needs to be done, this could be a reason to implement micro-frontends.
B. Collaborate with different frameworks in the frontend
Imagine you are inheriting a legacy application but want to build a new feature with modern design elements, then micro-frontends gives you a good head start.
C. Teams that have experience building Microservices application, and are willing to take it to the next step
Most of the points mentioned here are forward-thinking practices. Micro-frontends needs a good solid understanding of the domain and good discipline to contain things within one's boundary.
Finally, it's worth remembering that:
It's not a sprint. It's a marathon.
Micro-frontends adds significant overhead to the overall application. This isn't desired for smaller applications or for the application that will be built and managed by a single team. The above mentioned challenges are worth solving, only if you are up for the long run with multiple teams.
To view the recorded presentation, click here.