What Are Micro-frontends?

March 15, 2022

Micro-frontends is a type of software architecture for building distributed frontend applications. In this architecture, a frontend application is composed of smaller frontend apps that are LEAN:

  1. Loosely coupled
  2. Executed and deployed independently
  3. Autonomously developed and managed by a small team
  4. Narrowed to a business domain

LEAN is an acronym, that I’ve made up, for four design principles, intended to design micro-frontends that are maintainable, scalable, and lightweight.

As we’ll see in this post, microservices and micro-frontends share a lot of principles. In fact, the list of principles above could be used to describe microservices as well.

In this article I won’t cover the how and the why of micro-frontends. In this article I’ll focus only on what principles we can use to define what a micro-frontend is.

L Loosely coupled

Coupling normally refers to the degree to which code depends on other code. To build a loosely coupled architecture we must create strong boundaries. Creating strong boundaries between micro-frontends works because they are a barrier to references between micro-frontends.

However, coupling doesn’t only affect code. Coupling in a wider perspective can measure different types of dependencies.

Suppose two micro-frontends that depend on the following type definition:

function useLocale(): string

If each micro-frontend calls useLocale(), we would expect that each call in each micro-frontend returns the same value. Otherwise each micro-frontend would be displayed in a different language, which is probably not what we want. Therefore each micro-frontend doesn’t only depend on code, but also on some data that can change during execution time. We can think of that data as a state dependency.

Boundaries between two micro-frontends that use the same state

Let’s create a new version V2 of useLocale that has a required argument:

function useLocale(default: string) : string

This raises a question, which version of useLocale is a given micro-frontend depending on? If a micro-frontend calls useLocale V2 without passing the default required argument it will error. How do we fix that?

We need to create well defined interfaces between boundaries. This way we can provide the right dependencies in a given context. We can also avoid the previous issue by completely removing versioning, which I would not recommend for large projects.

Well defined interfaces can also help optimise what code needs to be downloaded at runtime. If two micro-frontends depend on the same code dependency and version we only need to download it once. Sharing dependencies helps make our micro-frontends lightweight.

We can also use well defined interfaces to share runtime dependencies such as data and execution context to make our micro-frontends more performant. Notice that this comes with its own trade-offs.

Failure to create strong boundaries with well defined interfaces will make our distributed apps dangerously unpredictable. Will you want to deploy your micro-frontends frequently if you are not confident that they won’t break the system? Probably not.

E Executed and deployed independently

Executed independently

Microservices are small services that are executed independently. They are small, even so, microservices are still standalone services.

Two different requests from an API gateway / BFF (Backend For Frontends) to two different microservices

Similarly to microservices and services, each micro-frontend app should still be, guess what, a standalone app. This may sound obvious but it’s important, an app is a program that can be executed independently. We should be able to execute micro-frontend apps independently just like any app.

Two different requests from a browser to two different micro-frontends

If micro-frontends can run as standalone applications it means that they are also independently developable, which improves developer experience. That may look like an optional nice-to-have for some people but being able to execute micro-frontends independently has an important side effect: it’s unlikely that a highly coupled micro-frontend is executed independently.

It’s possible to run highly coupled micro-frontends independently, but you’ll need to write custom code to handle different cases depending on the context, and it’ll get harder as the system grows. So, if you can easily execute your micro-frontends in isolation chances are that they are loosely coupled.

Deployed independently

Front-end monoliths can be broken up into packages that can be independently published to a registry, like npm for instance. If we can publish packages independently, why do we need micro-frontends? Before I answer that question, can you tell me what version of package X is request 1 going to execute in the following sequence diagram?

Diagram sequence. In step 1 package X version 1.0.0 is published to NPM. The monolith is built with package X version 1.0.0. Request 1 is sent from the user’s browser to the monolith

Request 1 executed version 1.0.0 of package X. Now, what version of package X is request 2 executing in the following sequence diagram?

Diagram sequence. In step 1 package X version 1.0.0 is published to NPM. The monolith is built with package X version 1.0.0. Request 1 from the user’s browser executes package X version 1.0.0. Package X version 1.0.1 is published to NPM. Request 2 is sent from the user’s browser to the monolith.

Both request 1 and request 2 execute version 1.0.0 of package X because the monolith was built only once. The monolith was not built after package X v1.0.1 was published.

Both packages and micro-frontends are integrated into a whole before users interact with them. Build time integration requires to building and deploying the whole when any of its parts are built and deployed. Thus, packages can’t be considered micro-frontends because they violate the executed and deployed independently principle.

Micro-frontends integration happens at runtime either on the server or on the browser. Micro-frontends are integrated into something often called a shell application or a container application. I prefer the term shell because it better indicates the idea of a lean layer. Also, the term container could be misleading in the context of front-end development.

Given the following diagram, in which a micro-frontend is being server-side integrated with a shell, what version of micro-frontend X is request 2 executing?

Diagram sequence. In step 1 micro-frontend X version 1.0.0 is deployed to some cloud storage. The user’s browser sends request 1 to the shell app. The shell app executes micro-frontend X version 1.0.0. Micro-frontend X version 1.0.1 is deployed. The user’s browser sends request 2 to the shell app. The shell app executes micro-frontend X version 1.0.1

Request 2 executed micro-frontend X version 1.0.1.

So, why are micro-frontend independent deployments so cool if we can also publish packages independently? Because with micro-frontends we can release and execute code independently without having to rebuild and redeploy the whole.

This type of runtime integration comes with its own challenges, which are not within the scope of this post.

A Autonomously developed and managed by a small team

Two subdomains, checkout and catalogue, owned by two different teams

If micro-frontends are independently developed, deployed, and executed it means that autonomous teams can own them end-to-end. This makes micro-frontends very scalable from a human resources perspective. It also reduces teams’ cognitive load and so increases dev productivity.

Micro-frontends work well when we have many teams working on different parts of the codebase. If we have a team responsible for the entire codebase, then the complexity that micro-frontends bring will increase their cognitive load and possibly reduce their productivity. In that case, it’s probably better if we start with a monolith first approach. We can upgrade to a micro-frontends architecture when the organisation is ready.

N Narrowed to a business domain

In the context of this post we are going to define “domain” as a sphere of knowledge, influence, or activity. Domains can contain subdomains:

Subdomains Checkout and Catalog are contained within the E-commerce domain

In many cases, domains on the frontend possibly match domains on the backend.

Vertical slicing - author Jimmy Bogard

The domains that we define should determine the boundaries of the micro-frontends that we design. Defining proper domains should reduce coupling between micro-frontends, and it’ll set clear team-micro-frontend relationships.

Sometimes in Web applications business domains are mapped to Internet subdomains. For instance podcasters.spotify.com and player.spotify.com could be two business domains within Spotify.

In some organizations, each Internet subdomain can be deployed as a micro-frontend. That view of micro-frontends can be limiting if we can’t have more than one page in an Internet subdomain where each page is a LEAN micro-frontend itself. For instance, Search could be a business subdomain within the Web Player business domain.

Two micro-frontends that are part of the same domain and are owned by the same team

In modern Web applications we may want to have a page that contains more than one business domain or subdomain. In the following example, the checkout page displays both a “checkout” micro-frontend owned by the Checkout team and also an “other customers also bought” micro-frontend owned by the Catalogue team.

Two micro-frontends that are part of the same domain and are owned by the same team

Defining the right domains and subdomains is a critical part of any software architecture, and micro-frontends are no different. I personally find this part very exciting.

If you are interested in learning more about LEAN micro-frontends don’t hesitate to subscribe to my newsletter.

Profile picture

Written by Alex Lobera, software engineer passionate about OSS, mentoring, and all things Web.

Subscribe to my newsletter

I won’t send you spam. Unsubscribe at any time.