Introduction
In software engineering, one of the most important technical decisions you can make is the method of partitioning a project’s architecture. The consequences of this decision cascade, so you must get this right! Unfortunately, there is no silver bullet solution as the correct decision depends on your context. As always, we defer to the First Law of Software Architecture:
“Everything in software architecture is a trade-off.”
Using micro-frontends is an increasingly popular method of partitioning frontend applications. In this post, I will define them and describe their trade-offs. For those who have more time, I will explore the why, starting with exploring different partitioning styles. I then share a coarse-grained overview of micro-frontends. But first, for those who are in a rush:
TL;DR Micro-frontends, inspired by microservices, break previously monolithic codebases into independently deployable parts that are composed of a greater whole. Deploying frontends independently improves desired characteristics such as scalability, agility, and maintainability. These benefits mirror those of micro-services but also have similar accompanying downsides: operational and governance complexities. Examples include complexities with code reuse, standardised UI, managing interactions between micro-frontends, and deployment configuration.
Partitioning
Before delving into micro-frontends, let us first explore other partitioning methods
Big Ball of Mud
One approach is the Big Ball of Mud partitioning (or lack thereof):
“A Big Ball of Mud is haphazardly structured, sprawling, sloppy, duct-tape, spaghetti-code jungle showing unmistakable signs of unregulated growth, and repeated, expedient repair.” — Brian Foote and Joseph Yoder
This approach is excellent for scrappy spikes and prototypes as it lends itself to characteristics of simplicity and (initial) speed. It is not ideal for longer term use, as the structure eventually reduces the characteristics of agility and resilience. Side point: an underrated skill is identifying the characteristics that matter to your particular context. Occasionally, code snobbery or business pressure leads teams to select incorrect structures or technologies that do not align with the characteristics that are needed to make the project a success.
Technical/Domain Partitioning
Most applications fall within the spectrum of Technical and Domain partitioned applications.
Technical partitioned applications are sliced into technical layers: persistence layers are separated from presentation layers, and so on. This simple structure allows for the optimisation and specialisation of technical layers. However, it means that business logic is smeared across layers, potentially resulting in lowered agility and resilience. Because of Conway's Law, an organisation with this partitioning structure usually has a separate database, backend, and frontend teams. In a frontend context, a technically partitioned application could be structured:
Domain partitioned applications are sliced into business domains. Think of Spotify model inspired cross-functional teams that own their product end-to-end. Or Amazon’s “You build it. You run it” approach. This has the benefits of increased agility, maintainability, and scalability. In a frontend context, applications with this partitioning are structured:
Monolithic/Distributed
Your decision to choose a particular partitioning method has consequences on the deployment possibilities. Technically partitioned architectures can only be monolithic, while domain partitioned architectures can be both monolithic or distributed. Monoliths are controversial in software engineering, but always be careful of dogma and have a trade-off mindset. Shopify, as an example, uses a modular monolith, which is perfectly suitable for its particular context.
If you are partial to fancy terminology, we can use the term “quantum”:
An architectural quantum is an independently deployable component with high functional cohesion
Monoliths have a quantum of one (everything is deployed together), and it is large. Modular monoliths also have a quantum of one. Micro-services have multiple quanta (many pieces can be deployed independently) and are also each smaller.
The reason for micro-service popularity? They are a more evolutionary architecture, as they have a higher quantity of smaller quanta. The trade-off, however, is in the resultant complexity. Deployment, code sharing, observability, and transactions are all areas that become more complex (although tools like Temporal are making life easier), so it is always a trade-off.
To go slightly off-piste, the trade-offs between decentralisation and centralisation touch on a wider theme of “competing methods of efficient data processing.” Free market economies process information in a decentralised fashion. Coherence emerges through parallel and independent actions of many individuals with limited and local knowledge. According to Yuval Noah Harari, this efficiency is why the Soviet Union’s inefficient centralised economy lagged behind the United States. Cryptocurrencies are another foray into the world of decentralisation that wrestles control away from central authorities which, naturally, have caused some alarm.
Micro-frontends
Micro-frontends are an emerging decentralised architecture style inspired by microservices. The idea is to break monolithic frontend codebases into smaller parts owned by autonomous teams. Unlike modular monolithic frontends, these applications are then independently deployed to production, where they are composed into a larger whole.
As a tangible example, see the diagram above. Team A and Team B work on completely separate domains that are then deployed independently and composed together with either a Vertical Split or Horizontal split partitioning style. In a Vertical split, teams own a business domain (see Domain-driven design), and in a Horizontal split, teams own a page slice.
This architecture can be used with various backend architectures, although they are particularly well suited to microservices. If used with microservices, cross-functional teams can fully own business domains.
Upsides
Tooling: teams can choose the best tool for their particular use case, refactor with less fear of creating bugs for other teams and rewrite applications easily. This helps move them move toward the working model of you build it, you run it, and it allows for incremental upgrades.
Deployment: teams can have “À la carte deployments” — each application is deployed to its own cadence. Unrelated changes do not need to be bundled together, and deployment risks are decreased, leading to increased agility.
Downsides
Governance: governing code quality standards and consistency across multiple teams is more difficult because of the reduced overall observability. The ability to mix a range of competing technologies, tools, or frameworks on a single page can also lead to micro frontend anarchy which can lead to poor performance and increased size.
Operational: Much more administrative work is required to manage micro-frontend tooling, pipelines, repositories, and servers. Collaboration and managing state is also more complex
Implementation Principles
If you do decide to implement Micro-frontends, there are a variety of foundational principles that will be needed in addition to the technology decisions. These principles are required to maximise the value of the following architectural characteristics micro-frontends provide: Resilience, Agility and Evolvability.
Focussed applications: Model applications around small business domains and have clear ownership of these applications amongst squads. This results in a decentralised but rapid evolution of the independent applications. Squads are highly aligned and loosely coupled from other teams and the code is simpler to understand, test and build. See Robert C. Martin’s Single-Responsibility Principle.
Deploy Independently: Ensure that applications can be deployed completely independently from one another. Squads should own their release process end-to-end and micro-frontends should never need to be deployed in a particular order. This decoupling results in faster iterations and increased evolvability.
Decentralize Governance: Decentralize the governance of micro-frontends to the squads themselves. This empowers developers to make the right technical decisions for the distinct problems they encounter. Ensure that there are cross-guild meetings centralize ideas, but not mandates, amongst squads. See the principle: Understand a problem individually, then share it.
Isolate Failure: Don’t share a run-time with other teams or have robust measures in place to either hide failing applications or provide alternative content in their place. See: Murphy's law.
Culture of Automation: Independent teams need a robust culture of automating the deployment of independent frontends in different environments. This allows teams to move in faster and more reliable way.
Highly Observable: Utilise tools such as Sentry to instrument micro-frontends and ensure that the squads have ample visibility into the metrics and alarms of their individual applications. See: Software should be easy to debug.
Conclusion
Micro-frontends are a fantastic way to manage complex frontend codebases. It is a partitioning style used by autonomous teams that improve characteristics of scalability and evolvability. They do not have the characteristic of simplicity and require considerable governance and DevOps investment. If your micro-frontends need to be deployed in a particular order, save yourself the trouble and stick to a modular monolithic frontend.
If the characteristics that your business requires align with those provided by micro-frontends, and if you have the operational expertise, it is worth exploring the method further. Migrating towards micro-frontends is also not a “big-bang” endeavour, so you can always spin up a lighthouse team to carve off a small domain as a first experimental step.
This article was deliberately coarse-grained to examine the principles of micro-frontends. If you wish to have a more fine-grained view, I recommend looking at the following resources:
NX: a tool that provides out-of-the-box micro-frontend support
Cam Jackson’s fantastic deep dive into micro-frontends
ThoughtWorks writings on micro-frontends
Hello Fresh’s micro-frontend implementation
Micro-frontends on AWS
Book: Building micro-frontends