In the first of our new series on micro frontends, we look at what they are and the benefits & challenges that they offer.
We will also cover, as part of this series, why you might want to adopt micro frontends, various implementation patterns, some of the trade-offs to be aware of and finally some case studies of implementing them with our clients.
So, what are micro frontends?
At a high level, micro frontends is a user interface (or UI) architectural style wherein a single UI is decomposed into smaller, independently deployable components. This architectural style can be beneficial in helping to make it easier to make changes to the software as well as isolating any changes.
Micro frontends borrow the naming from microservices, which has gained a lot of traction over recent years. The key characteristics of a microservices architecture are loosely coupled and independently deployable components forming a larger system. Using the same definition we can consider the essential properties of micro frontends to be loosely coupled and independently deployable frontend components forming a larger frontend.
How micro frontends can help decouple parts of a user interface
When designing software systems, it is important to always consider the level of coupling between components. High coupling between components also makes it difficult for multiple teams to work as autonomously as possible. This results in a need for more coordination between teams which increases organisational complexity and results in a general slowdown in the speed of delivery and an increase in the cognitive load for teams.
One of the most common areas for high coupling tends to be the user interface. It is the layer in which data is pulled together in order to be presented back to users in a meaningful and useful manner.
Micro frontends aim to reduce the level of coupling between parts of a user interface. As a result, the different parts can evolve somewhat independently and have independent deployments. Micro frontends can also reduce coupling between teams. By decomposing a user interface into multiple, smaller components we can then divide the ownership of these up appropriately. In doing so, we can remove dependencies between teams such as aligning on deployments, sequencing of work and agreeing on contracts for exchanging data between systems that cross team boundaries. We shall cover the topic of teams and ownership in more detail, in a future article later in this series.
What are the benefits of micro frontends?
The flexibility of Technology Choice
One of the potential benefits of adopting a micro frontend strategy is that it offers the option to use multiple frameworks. Although sometimes regarded as an anti-pattern due to the fact that different frameworks aren’t inherently designed to be bootstrapped together in the same browser tab, there are some scenarios where this may be needed. These scenarios include working with legacy systems, migrating to a new frontend framework/library or integrating two disparate systems (for example as a result of a company acquisition or merger). Having the flexibility of being able to combine multiple frameworks in these situations can be beneficial.
Another of the benefits provided by adopting micro frontends is that they introduce the possibility of independent deployments. Meaning, each micro frontend can be deployed separately. This results in a reduction of the scope of any one particular deployment where deployments become inherently less risky because there is less change introduced by each one.
As is the case with microservices, each micro frontend can and should have its own continuous delivery pipeline. An automated mechanism for its build, test and subsequent deployment to all available testing environments and production. Teams owning a micro frontend should not need to consult with owners of other micro frontends to align on deployments. Each micro frontend is able to be deployed as soon as it is ready for production and at a cadence chosen by the team owning it. This removes the need for costly coordination across teams and ensures that changes can be deployed into production in a more timely manner, rather than having to adhere to any fixed release cycle which aligns other teams.
The codebase for any given micro frontend will be, by its very nature, much smaller than that of say a monolithic application or frontend. This reduction in size reduces the cognitive load on developers that to need work with it, making it easier to manage and make changes to. Separating these micro frontends out into their own codebases also provides us with a level of protection against unintended coupling. Teams are much less likely to share domain models, for example across codebases.
Perhaps one of the more strategic benefits is that adopting a micro frontends architectural strategy and introducing independent deployments and codebases also sets us up really well for having engineering teams that are fully independent from one another and able to function autonomously. This means teams are able to own a full vertical slice from User Interface right down to data persistence. Where ‘own’ here means from defining the work that needs to be done right through to deployment of that work into a production environment. There will likely be further architectural and process changes needed to fully realise this but adopting micro frontends provides a lot of the foundational pieces needed.
Having engineering teams that are autonomous, enables a greater level of agility. Having full ownership of a vertical slice enables teams to move quickly, without the cost of having to coordinate with other teams around roadmaps, backlogs and deployments. To achieve this level of autonomy though teams should be formed around vertical slices of business functionality, rather than around technical concerns. For example a Payments team rather than a UI team. Matthew Skelton and Manuel Pais refer to this as “stream-aligned teams” in their book Team Topologies, describing these types of teams as being “aligned to a single, valuable stream of work”.
What are the challenges of micro frontends?
Although micro frontends can provide a lot of benefits, like most things in life it doesn’t come for free. There is added complexity in splitting the frontend into smaller pieces and if not considered correctly can cancel out the benefits.
Getting the granularity right
The micro frontend split can be too granular or too coarse-grained. There is effort involved in splitting micro frontends - especially if there is a change of ownership involved. Just like any other component boundaries, it is useful to see them through the lens of coupling and cohesion. Granularity has a direct impact on the complexity of communication between components. Components that have complex communication patterns with other components that cross team boundaries would require more complex relationships between those teams.
For example, one method of splitting out micro frontends is through the Orchestrator pattern. We will discuss this further in a later article but in a nutshell an Orchestrator would be a container or page that hosts different micro frontend components.
There are decisions to be made on a technical and organisational level about whether to build and maintain an orchestrator component to host the other micro frontends or whether that responsibility should live with the primary component on the page. If an orchestrator is chosen the decision about which team or teams should own it is a decision worth making together and reviewing periodically.
Cross-team orchestration is an important point to consider. It is quite easy for responsibilities to bleed into other bounded contexts if the integrity of those micro frontends is not well governed.
For example, let us consider an e-commerce company that has Product Listing and Finance-related teams. Each one produces frontend components for the website. Product Listing only needs to concern itself with displaying information regarding product details and so should leave areas of the UI available for other teams like Finance in this example to populate with their frontend component(s). This requires careful coordination between teams so that the responsibilities on what to display on a page are well managed and understood.
It is important to consider how developers are able to visualise the whole application on their local machines. In less desirable setups developers have to use a separate test environment to see their changes in the context of the whole application. This should be avoided. Local setup is very important in developer productivity and should allow offline setup as much as possible.
Depending on the type of integration approach, a harness can be built to hold your micro frontend(s) to stub-out (or fake) its external dependencies. This can be an efficient environment for local development.
Often organisations and software development teams that adopt a microservices architecture for the first time fail to fully understand the tradeoffs at play. A key tradeoff is around deployment and orchestration. The same holds true for the adoption of micro frontends. Like microservices, micro frontends demand much more engineering maturity around deployment processes.
With the more traditional monolithic user Interface, there is just a single deployment process required in order to deploy. When adopting a micro frontend architecture, there now needs to be individual deployment processes for each micro frontend component. Moreover, these processes need to be decoupled from each other to allow differing deployment cadences and possibly even ownership by different teams. In order to achieve this, there needs to be a very good understanding of the dependencies between each micro frontend module.
All of this means that we need to invest in ensuring that we have as much automation as possible but also the right level of testing within the deployment processes to provide us with sufficient confidence that a change to a micro frontend is safe to go into production.
Whilst these things can be challenging, especially when dealing with them for the first time, it is good to recognise the wide-ranging benefits that come from meeting the demands of this pattern. Micro frontends encourage a mature and responsible engineering culture where teams can enjoy autonomy, have greater ownership over what they are building and delivering to customers, and are required to understand their application in relation to the wider system
Complex communication between components
When communication between different micro frontends becomes complex, as with any other software, it is worth reflecting on the level of coupling and cohesion between those components. We will discuss the topic of handling changes to component boundaries in a later article in this series. For now, let us highlight a couple of situations where complex communication between micro frontend components in the UI layer can occur:
A component that is integrated at build-time might initialise itself with information that can only come from the component that houses it, at run time.
Another common scenario is a page that contains multiple components which share a common state. Each component offers the user a way to update that state. How an application orchestrates such a data flow can have implications that seriously affect the performance of the page experience, which can impair the overall user experience.
There needs to be a clear strategy for how micro-frontend components will communicate with each other. This will need to cater for all of the different types of components that might exist within your micro frontend architecture. Whether components are integrated during build, deployment or runtime, available inputs and outputs should be clearly defined, in other words, each component offers an API allowing other components to interact with it. Another sensible concept for handling complex communication is the orchestrator. A simple orchestrator could take the form of a parent component hosting multiple children. Other orchestrators define more complex relationships and can manage the sharing of data between multiple micro frontends.
Whichever frontend architectural pattern is chosen, we want that to be invisible to the users. We want our users to believe they are interacting with a single application even if in reality it might be composed of multiple independently developed components. A key part of achieving this is having consistent styling and user experience throughout. Composing a single application from multiple code repositories can make this challenging.
Adopting a design system across the whole application is one way to remediate this. A design system typically defines atomic-level styles and user controls that can be shared and re-used across any number of components. When a styling change is made it should update consistently across the whole application as each component consuming the design system is updated to use the latest version. Care must be taken when planning this kind of collaboration. However, as with many of the micro frontend challenges, the solutions have been around for a long period of time and will be familiar to mature teams – in this case, semantic versioning, considerate release notes and good cross-team communication make all the difference.
Additionally, there needs to be a clear strategy for how the user interface style (i.e the CSS will be managed by the micro frontend components – in particular when it comes to global styling (styles across an entire application) and local styling (styles for an individual micro frontend component). This is to avoid CSS rules overriding one another, which can have a detrimental effect on both user and developer experience.
Standardising development approach
Whilst adopting a micro frontend architecture can allow individual teams to have a greater level of autonomy, some degree of governance and standardisation of this can be highly beneficial. Without this, certain problems can arise. For example, with teams now free to choose their own frontend technologies this could create knowledge silos across an organisation. Similarly, if different engineering teams have adopted many different technologies it can make it much more difficult for individuals to move between teams or projects. Also, along this greater level of autonomy teams are now able to develop and build frontend components in isolation. Whilst this has clear benefits, it can lead to differing expectations around development approaches such as coding conventions, standards around coding quality etc. Whilst some degree of this is good, there still needs to be a level of governance to ensure that quality across engineering teams is maintained and certain key approaches are standardised.
Some solutions that can work well here are adopting static code analysis tooling and having a common standard for its configuration. One of our clients, Cazoo created a shared package that defined a set of linting rules that could be incorporated into different codebases across the organisation. This ensured consistency of coding styles and avoided duplication of linting rules. Having a clear organisation-wide tech strategy can also provide guardrails for engineering teams to ensure that they are adhering to the right technology choices but also have a degree of autonomy to make certain types of decisions themselves. Finally, with more autonomous teams it’s important to then create platforms for knowledge sharing. Internal Communities of Practice often work well for this.
Testing across components
A micro frontend architecture allows teams to test their components thoroughly in isolation from other components. Teams can choose the strategy and which testing frameworks to use independently from each other so that it best suits their specific needs. In most cases, a micro frontend architecture simplifies testing, since it is easier to test smaller modules in isolation than a single larger monolith.
The biggest complexity lies in verifying that independently developed and deployed micro frontends are going to work seamlessly together, as a whole. Each micro frontend should ideally have its own comprehensive suite of automation tests, however, these only verify the behaviour of an individual micro frontend. There still needs to be some level of testing across micro frontend components to verify their integration.
One approach to test the integrations is to use Contract Tests. This methodology allows us to strictly focus on testing the interface boundaries (or “contracts”) between micro frontends. This has a big advantage because these kinds of tests are fairly simple and fast to run. This solution also helps teams to maintain a clean and visible interface between their micro frontend and its neighbours. These types of tests simply ensure that the interface contract between micro frontends is being upheld.
Another approach is to create automated end-to-end tests. The main concerns with this solution is that these kinds of tests are usually long-running, complex, often brittle and expensive to maintain. If end-to-end tests are used at all they should just focus on validating that the integrated web app works correctly.
Like with any architectural pattern, security needs to be carefully considered and micro frontends are no different. In fact, micro frontends introduce some specific security challenges that should be understood before adopting them.
Firstly, a micro frontend architecture requires that each individual component is secure. If any one component were to contain a security vulnerability then it could compromise the entire application. This means that each individual team working on a component needs to take care to ensure their component is secure.
In a similar vein, particular care needs to be taken around how micro frontend components interact with each other. Specifically, how these components share data and resources between them. Correct security mechanisms need to be in place to ensure that only those authorised components can access data and resources from other components and that any sensitive data is not exposed to other components or the browser where it should not be.
Cross-Origin Resource Sharing (or CORS) can also be a concern with a micro frontend architecture if individual components are served from different domains. Web browsers quite rightly prevent this, by default. If this is the case, the appropriate CORS headers and policies need to be configured to specifically allow these integrations.
Libraries and dependencies at different levels and versions
Another one of the drawbacks of adopting micro frontends can be the additional complexity that appears when managing libraries and dependencies. It is a common situation that the micro frontends within a project are going to use the same core libraries like for example React or Redux. These libraries may be bundled into the micro frontend component production code.
Because of that, the amount of data that the end user needs to download is much larger than in a monolithic architecture. This can impact the performance and may cause poor user experience. Webpack’s Module Federation provides a solution to this by providing a way to share common dependencies amongst multiple “modules”.
Another problem appears when micro frontends have dependencies with different versions of the same libraries. This adds a level of complexity and can even lead to situations where the system fails to work due to the use of incompatible versions. Some of the solutions discussed for standardising development approaches are relevant here as well. As with many of the issues, the solutions carry their own benefits – mature teams, responsible ownership and good communication.
Another potential issue is that differing micro frontend components could be leveraging different libraries or different versions of the same library. This opens up a potential security risk. It is vital to have a governance policy around the use of third-party libraries and frameworks to track which versions are being used and to have a process for updating those when needed. Thankfully automated tools like Dependabot and Synk exist that cross-reference dependency versions against lists of known vulnerabilities and can take a lot of the manual work out of keeping your third-party libraries up to date.
The constantly evolving landscape of micro frontends
Despite its growing popularity over recent years, this architectural pattern is still relatively new. As a result, there is a lot of research still being conducted around the topic and many practices are still yet to emerge and become established.
As a result of the recent growth in popularity and its immaturity relative to some other architectural patterns, there have been many different approaches to implementing micro frontends. This can cause difficulties for teams and organisations wishing to adopt micro frontends for the first time and looking for guidance from the wider industry. A similar situation occurred for example when the micro services architecture pattern started to emerge.
As with any new technology or pattern, the tooling and support for it takes time to develop and mature. The same is true for micro frontends. For example, it is often discussed that there is a lack of tooling for routing communication between micro frontend components. As a result, teams more often than not need to build this internally.
This article has attempted to provide an introduction to micro frontends as a frontend architecture pattern. When making a decision that impacts a software system’s architecture it is vital to consider the trade-offs at play specific to your own situation. Different teams and organisations will have their own set of architectural needs and as a result, whilst adopting micro frontends might be a good choice for one, it will not necessarily be the best choice for every organisation. Throughout this article, we have shone a light on what we believe some of the main benefits and challenges of micro frontends are, and touched upon the solutions to some of the challenges. To recap, here is a break down:
Benefits of micro frontends:
- Autonomous teams
- Flexibility of technology choice
- Independent deployments
- Smaller codebases
Challenges of micro frontends:
- Communication between components can be complex
- Consistent styling
- Constantly evolving landscape of micro frontends
- Domain boundaries are more expensive to change
- Extra considerations around deployment
- Sharing Libraries and dependencies at different levels
- Local setup needs extra work
- Security challenges spread wider
- Standardising development approach
- Testing across components