Chapter 1. Micro-Frontends Principles

At the beginning of my career, I remember working on many software projects where small or medium-size teams were developing a monolithic application with all the functionalities of a platform available in a single artifact, the product produced during the development of a software, and deployed to a web server.

When we have a monolith, we write a lot of code that should harmoniously work together. In my experience, we tend to pre-optimize or even over-engineer our application logic more often than not. Abstracting reusable parts of our code can create a more complex codebase and sometimes the effort of maintaining a complex logic doesn’t pay off in the long run. Unfortunately, something that looked straightforward at the time could look very unwieldy a few months later.

In the past decades, public cloud providers like Amazon Web Services (AWS) or Google Cloud started to gain traction. Nowadays they are popular for delegating what is increasingly becoming a commodity, freeing up organizations to focus on what really matters in a business: the services offered to the final users.

While cloud systems offer easier scalability compared to on-premise infrastructure, monolithic architectures require us to scale either horizontally adding more containers or virtual machines or vertically increasing the configuration of the machine where our application is running.

Furthermore, working on a monolith codebase with distributed teams and co-located ones could be challenging as well. Particularly after reaching medium or large team sizes because of the communication overhead and centralized decisions where a few people decide for everyone.

In the long run, companies with large monoliths usually slow down all the operations needed to release any new feature, losing the great momentum they had at the beginning of a project where everything was easier and smaller with few complications and risks. Also, with monolithic applications, we have to test and deploy the entire codebase every single time, which comes with a higher chance of breaking the APIs in production, introducing new bugs, and making more mistakes, especially when the codebase is not rock solid or extensively tested.

Solving these and many other challenges its staff faces, a company might move from complex monolith codebases to multiple smaller codebases and scoped domains called microservices.

Nowadays microservices architecture is a well-known, established and popular pattern used by many organizations across the world.

Microservices split a unique codebase into smaller parts, each of them with a subset of functionalities compared to a monolith. This business logic is embraced by developers because the problem solved by a microservice is simpler than looking at thousands of lines of code. Moreover a developer can maintain a clear picture of the code base and related functionality implemented, considering the cognitive load is by far less than working on a monolithic system.

Another significant advantage is that we can scale part of the application and use the right approach for a microservice instead of a one-size-fits-all approach similar to a monolith.

There are also some pitfalls to working with microservices. The investment on automation, observability, and monitoring has to be completed to have a distributed system under control. Another pitfall is the wrong definition of a microservice’s boundary, for instance, having a microservice that is too small for completing an action inside a system relying on other microservices causing a strong coupling between services and forcing them to be deployed together every time. When this phenomenon is extended across multiple services we risk ending up with a big ball of mud or a system that is so complex that it is hard to extend.

Microservices bring many benefits to the table but could bring many cons as well. In particular, when we are embracing them in a project, the complexity of having a microservice architecture could become more painful than beneficial. Considering the options available in software architecture, we should pick microservices only when needed and not choose them recklessly just because it is the latest and greatest approach.

Micro-frontends have gained more traction in the frontend community and enterprise organizations thanks to the great fit they have when aligned to other distributed architectures like microservices. Keep in mind, however, that just like how microservices aren’t a universal answer to all software decomposition, neither are micro-frontends. To understand where they fit in and what they are, let’s look at some of the forces that are pushing us in this direction.

Monolith to Distributed Systems

When we start a new project or even a new business offering a service online, the first iteration should be used to understand if our project or business could succeed or not.

Usually, we start by identifying a tech stack, a list of tech services used to build and run a single app, that is familiar to our team. By minimizing the bells and whistles around the system and concentrating on the bare minimum we’re able to quickly gather information about our business idea directly from our users. This is also called a minimum viable product (MVP).

Often we design our API layer as a unique codebase (monolith) so we need to set up a single continuous integration or continuous delivery pipeline for the project. Integrating observability in a monolith application is quite easy; we just need to run an agent per virtual machine or container to retrieve the health status of our application servers. The deployment process is trivial, considering we need to handle one automation strategy for the entire APIs layer, one deployment and release strategy and when the traffic starts to increase we can scale our machine horizontally, having as many application servers as needed to fulfill the users’ requests.

That’s also why monolithic architecture are often a good choice for new projects considering we can focus more on the business logic of an application instead of investing too much effort on other aspects such as automation for instance.

Where are we going to store our data? We have to decide which database better suits our project needs—a graph, a NoSQL, or a SQL database? Another decision that must be made is whether we want to host our database on a cloud service or on-premises. We should select the database that will fit our business case better.

Finally, we need to choose a technology for representing our data, such as within a desktop or mobile browser, or even a mobile application. We can pick the best-known JavaScript framework available or our favorite programming language; we can decide to use server-side rendering or a Single Page Application architecture; then we define our code conventions, linting, and CSS rules.

At the end, we should end up with what you can see in Figure 1-1:

3 tiers architecture composed by a presentation layer  frontend   an application layer  APIs layer  and a persistent layer  database
Figure 1-1. 3-tiers architecture composed by a presentation layer (frontend), an application layer (APIs layer) and a persistent layer (database)

Hopefully, the business ideas and goals behind our project will be validated and more users will subscribe to our online service or buy the products we sell.

Moving to Microservices

Now imagine that thanks to the success of our system, our business decides to scale up the tech team, hiring more engineers, QAs, scrum masters, and so on.

While monitoring our logs and dashboards, we realize not all our APIs are scaling organically. Some of them are highly cacheable, so the content delivery networks (CDNs) are serving the vast majority of the clients. Our

origin servers are under pressure only when our APIs are not cacheable. Luckily enough, they’re not all our APIs, just a small part of them.

Splitting our monolith starts to make more sense at this point, considering the internal growth and our better understanding of how the system works.

Embracing microservices also means reviewing our database strategy and, therefore, having multiple databases that are not shared across microservices; if needed, our data is partially replicated, so each microservice reduces the latency for returning the response.

Suddenly we are moving toward a decentralized ecosystem with many moving parts that are providing more agility and less risk than before.

Each team is responsible for its set of microservices. Team members can make decisions on the best database to choose, the best way to structure the schemas, how to cache some information for making the response even faster, and which programming language to pick for the job. Basically, we are moving to a world where each team is entitled to make decisions and be responsible for the services they are running in production, where a generic solution for the entire system is not needed besides the key decisions, like logging and monitoring, as we can see from Figure 1-2.

Microservices with Single Page Application
Figure 1-2. Microservices with Single Page Application

However, we are still missing something here. We are able to scale our APIs layer and our persistent layers with well-defined patterns and best practices, but what happens when our business is growing and we need to scale our frontend teams, too?

Introducing Micro-Frontends

So far on the frontend, we didn’t have many options for scaling our applications, for several reasons. Up to a few years ago, there wasn’t a strong need to do so because having a fat server, where all the business logic runs, and a thin client, for displaying the result of the computation made available by the servers, was the standard approach.

This has changed a lot in the past few years. Our users are looking for a better experience when they are navigating our web platforms, including more interactivity and better interactions.

Companies have arisen providing services with a subscription model, and many people are embracing those services. Now it’s normal to watch videos on demand instead of on a linear channel, to listen to our favorite music inside an application instead of buying CDs, to order food from a mobile app instead of calling a restaurant.

This shift of behaviors requires us to improve our users’ experience and provide a frictionless path to accomplish what a user wants without forgetting quality content or services.

In the past we would have approached those problems by dividing parts of our application in a shared components library, abstracting some business logic in other libraries so they could be reused across different parts of the application. In general, we would have tried to reuse as much code as possible.

I’m not advocating against solutions that are still valid and fit perfectly with many projects, but we might encounter quite a few challenges when embracing them.

For instance, when we have multiple development teams, all the rules applied to the codebase are often decided once, and we stick with them for months or even years because changing a single decision would require a lot of effort across the entire codebase and be a large investment for the organization without providing any value for the customers or the company.

Also, many decisions made during the development could result in trade-offs due to lack of time, ideal consistency, or simply laziness. We must consider that a business, like technology, evolves at a certain pace and it’s unavoidable.

Code abstraction is not a silver bullet either; prematurely abstracting code for reuse often causes more problems than benefits. I have frequently seen abstractions make code thousands of times more complicated than necessary to be reused just twice inside the same project. Many developers are prone to over-engineering some solutions, thinking they will reuse them tens of times, but in reality, they use them far fewer times. Using libraries across multiple projects and teams could end up producing more complexity than benefits such as making the codebase more complex or requiring more effort on manual testing or adding overhead in communications.

We also need to consider the monolith approach on the frontend. Such an approach won’t allow us to improve our architecture in the long run, particularly if we are working on platforms meant to be available for our users for many years or if we have distributed teams in different time zones.

Asking any business to extensively revise the tech it uses will cause a large investment upfront before it gets any results.

Now the question becomes quite obvious: Do we have the opportunity to use a well-known pattern or architecture that offers the possibility of adding new features quickly, evolving with the business, and delivering part of the application autonomously without big-bang releases?

I picture something like Figure 1-3:

Micro architectures combined  this is a high level diagram showing how Microservices and Micro frontends can live together
Figure 1-3. Micro-architectures combined, this is a high-level diagram showing how Microservices and Micro-frontends can live together

The answer is YES!

We can definitely do it and it’s where micro-frontends come to the rescue.

This architecture makes more sense when we deal with mid-large companies and during the following chapters, we are going to explore how to successfully structure our micro-frontends architectures.

However, first we need to understand what the main principles are behind micro-frontends to leverage as guidance during the development of our projects.

Microservices Principles

At the beginning of my journey into micro-frontends in 2016, there wasn’t any guidance on how to structure such architecture, therefore I had to take a step back from the technical implementation and look at the principles behind other architectures for scaling a software project. Would those principles be applicable to the frontend too?

Microservices’ principles offer quite a few useful concepts. Sam Newman has highlighted these ideas in his book - Building Microservices (O’Reilly). I’ve summarized the theories in Figure 1-4:

Microservices principles
Figure 1-4. Microservices principles

Let’s discuss the above principles and see how they apply to the frontend.

Modeled Around Business Domains

Modeling around business domains is a key principle brought up by domain-driven design (DDD). It starts from the assumption that each piece of software should reflect what the organization does and that we should design our architectures based on domains and subdomains, leveraging ubiquitous languages shared across the business.

When working from a business point of view, this provides several benefits, including a better understanding of the system, an easier definition of a technical representation of a business domain, and clear boundaries on which a team should operate.

Culture of Automation

Considering that microservices are a multitude of services that should be autonomous, we need a robust culture of automating the deployment of independent units in different environments. In my experience, this is a key process for leveraging microservices architecture; having a strong automation culture allows us to move faster and provide a better feedback loop for developers that will relay to all the capabilities offered by the company in terms of security and performance guardrails that are part of the continuous integration process.

Hide Implementation Details

Hiding implementation details when releasing autonomously is crucial. If we are sharing a database between microservices, we won’t be able to change the database schema without affecting all the microservices relying on the original schema. DDD teaches us how to encapsulate services inside the same business domain, exposing only what is needed via APIs and hiding the rest of the implementation. This allows us to change internal logic at our own pace without impacting the rest of the system. Very often, we call this approach API-First. We begin by defining the APIs, which serve as the contract binding the producer and consumer(s) teams. This allows them to work in parallel, focusing on either producing or consuming the specified contract. By focusing on the API early in the development process, teams can enhance collaboration, scalability, and adaptability, making it easier to integrate and extend functionalities as the project evolves.

Decentralize All the Things

Decentralizing the governance empowers developers to make the right decision at the right stage to solve a problem. With a monolith, many key decisions are often made by the most experienced people in the organization. These decisions, however, frequently lead to trade-offs alongside the software lifecycle. Decentralizing these decisions could have a positive impact on the entire system by allowing a team to take a technical direction based on the problems they are facing, instead of creating compromises for the entire system. Bear in mind that in distributed systems a team has less cognitive load to carry, therefore each team member quickly becomes a domain expert in a portion of the system and can provide the best decision to evolve its own domain.

Deploy Independently

Independent deployment is key for microservices. With monoliths, we are used to deploying the entire system every time, with a greater risk of live issues and longer times for deploying and rolling back our artifacts. With microservices, however, we can deploy autonomously without increasing the possibility of breaking our entire API layer. Furthermore, we have solid techniques, like blue-green deployment or canary releases that allow us to release a new version of a microservice with even less risk, which clears the path for new or updated APIs.

Isolate Failure

Because we are splitting a monolith into tens, if not hundreds, of services, if one or more microservices becomes unreachable due to network issues or service failures, the rest of the system should be available for our users. There are several patterns for providing graceful failures with microservices and the fact that they are autonomous and independent just reinforces the concept of isolating failure.

Highly Observable

One reason that you would favor monolithic architecture in comparison to microservices is that it is easier to observe a single system than a system split in multiple services. Microservices provide a lot of freedom and flexibility, but this doesn’t come for free; we need to keep an eye on everything through logs, monitors, and so on. For example, we must be ready to follow a specific client request end to end inside our system. Keeping the system highly observable is one of the main challenges of microservices.

Embracing these principles in a microservices environment will require a shift in mindset not only for your software architecture but also for how your company is organized. It involves moving from a centralized to a decentralized paradigm, enabling cross-functional teams to own their business domains end to end. This can be a particularly huge change for medium to large organizations.

Applying Principles to Micro-frontends

Now that we’ve grasped the principles behind microservices, let’s find out how to apply them to a frontend application.

Modeled Around Business Domains

Modeling micro-frontends to follow DDD principles is not only possible but also very valuable. Investing time at the beginning of a project to identify the different business domains and how to divide the application will be very useful when you add new functionalities or depart from the initial project vision in the future. DDD can provide a clear direction for managing backend projects, but we can also apply some of these techniques on the frontend. Granting teams full ownership of their business domain can be very powerful, especially when product teams are empowered to work with technology teams. The primary difference between a micro-frontend and a component lies in their modularization approach. A micro-frontend completely owns a business domain, whereas a component focuses on addressing a technical challenge, often characterized by code duplication or the creation of complex, configurable components used across multiple domains. The component approach exposes an API that is frequently coupled with its container. Therefore, any modification made to the component is likely to impact its containers as well, creating an unwanted coupling that prevents it from reaching the principles behind distributed systems. With micro-frontends, we streamline the API surface to the essential minimum required for comprehending the user’s context. Typically, micro-frontends require little beyond accessing a session token and other pertinent information such as a product ID. This approach effectively diminishes the coupling between elements of the frontend application and enhances team autonomy by reducing the need for coordination across teams, owing to the infrequent changes in the minimal API exposed.

Culture of Automation

As for the microservices architecture, we cannot afford to have a poor automation culture inside our organization; otherwise any micro-frontends approach we are going to take will end up a pure nightmare for all our teams. Considering that every project contains tens or hundreds of different parts, we must ensure that our continuous integration and deployment pipelines are solid and have a fast feedback loop for embracing this architecture. Investing time in getting our automation right will result in the smooth adoption of micro-frontends and will solve common challenges like aligning shared libraries to a specific version, enforcing budget size per micro-frontend or forcing to update every micro-frontend to the latest design system version. Moreover, automation is not important only for generating technical artifacts, more importantly it provides a fast feedback loop for developers. Creating fast and helpful feedback loops for developers will foster the righs behaviors inside the teams enforcing important architecture characteristics across the distributed system.

Hide Implementation Details

Hiding implementation details and working with contracts are two essential practices, especially when parts of our application need to communicate with each other. It’s crucial to define upfront an API contract that is shared across the teams who need to interact with different micro-frontends. Also, strong encapsulation is required to avoid domain leaks in other parts of the application. In this way each team will be able to change the implementation details without impacting other teams unless there is an API contract change. These practices allow a team to focus on the internal implementation details without disrupting the work of other teams. Each team can work at its own pace, drastically reducing external dependencies and creating more effective collaboration.

Decentralization over Centralization

Decentralizing a team’s decisions finally moves us away from a one-size-fits-all approach that often ends up being the lowest common denominator. Instead, the team will use the right approach or tool for the job. As with microservices, the team is in the best position to make certain decisions when it becomes an expert in the business domain. This doesn’t mean each team should take its own direction but rather that the tech leadership (architects, principal engineers, CTOs) in conjunction with the developers and practices applied in the field, should provide guardrails between which teams can operate without needing to wait for a central decision. This leads to a sharing culture inside the organization becoming essential for introducing successful practices across teams.

Deploy Independently

Micro-frontends allow teams to deploy independent artifacts at their own speed. They don’t need to wait for external dependencies to be resolved before deploying. Achieving independence in micro-frontends means not reducing the user interface to mere components. We need to reduce the external dependencies for a team, in this way we optimize for a fast flow that will enable a team to run their operations independently.

When we combine this approach with microservices, a team can own a business domain end to end, with the ability to make technical decisions based on the challenges inside their business domain rather than finding a one-size-fits-all approach.

Isolate Failure

Isolating failure in SPAs, for instance, isn’t a huge problem due to their architecture, but it is with micro-frontends. In fact, micro-frontends require composing a user interface at runtime, which may result in network failures or 404 errors for one or more parts of the UI. To avoid impacting the user experience, we must provide alternative content or hide a specific part of the application. This might result in gracefully hiding non-essential micro-frontends from the interface if they fail or return a 500 error, in case the main micro-frontend of a page is not loaded.

Highly Observable

Frontend observability is becoming more prominent every day, with tools like Sentry, New Relic or LogRockets providing great visibility for every developer. Using these tools is essential to understanding where our application is failing and why. As Werner Vogels, Amazon’s CTO, used to say: “everything fails all the time”, therefore being able to resolve issues quickly is far more important than preventing problems. This moves us toward a paradigm where we can better invest our resources by remaining ready to address system failures rather than trying to prevent them completely. As with all microservices’ principles, this is applicable to the frontend, too.

The exciting part of recognizing these principles on the frontend and backend is that, finally, we have a solution that will empower our development teams to own the entire range of a business domain, offering a simpler way to divide labor across the organization and iterate improvements swiftly into our system.

When we start this journey into the micro-world we need to be conscious of the level of complexity we are adding to a project, which may not be required for any other projects.

There are plenty of companies that prefer using a monolith over microservices because of the intrinsic complexity they bring to the table. For the same reason, we must understand when and how to use micro-frontends properly, as not all projects are suitable for them.

Micro-frontends are not a silver bullet

It’s very important that we use the right tool for the right job. I cannot stress this point enough. Too often I have seen projects failing or drastically delayed due to poor architectural decisions.

We need to remember that:

Note

Micro-frontends are not appropriate for every application because of their nature and the potential complexity they add at the technical and organizational levels.

Micro-frontends are a sensible option when we are working on software that requires an iterative approach and long-term maintenance, when we have projects that require a large development team, in multi-tenant applications, or when we want to replace a legacy project in an iterative way.

However, they are not suitable for all frontend applications, they are an additional available option of frontend architecture for our projects. Micro-frontends architecture has plenty of benefits but also has plenty of drawbacks and challenges. If the latter exceed the former, micro-frontends are not the right approach for a project. As Neal Ford and Mark Andrew Richards have described in their book Software Architecture, “Don’t try to find the best design in software architecture, instead, strive for the least worst combination of trade-offs.” This should be your mantra from now on!

Summary

In this chapter we introduced what micro-frontends are, what their principles are, and how those principles are linked to an architecture like microservices that was created for solving similar challenges.

Next, we will explore how to structure a micro-frontend project from an architectural point of view and the key technical challenges to understand when we design our frontend applications using them.

Get Building Micro-Frontends, 2nd Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.