As web applications grow more sophisticated, we need strategies for breaking down their complexity into manageable chunks. Micro Frontends is an architectural pattern for creating large applications out of small applications while maintaining a consistent user experience. This approach enables us to build, test, and deploy individual chunks of functionality. The composability of micro frontends results in reduced complexity while more effectively scaling our most important asset: people.
Key benefits of micro frontends include:
Smaller, more maintainable codebases (Jackson, C.)
Scalable organizations made up of decoupled, autonomous teams (Jackson, C.)
Upgrade or replace parts of the interface in an incremental fashion (Jackson, C.)
Run and adopt experiments with limited risk
Core ideas of micro frontends include:
Isolated applications: independent runtimes even if all teams share the same framework (Geers, M.)
Isolated state: each application manages its own state (Geers, M.), with global communication happening over lightweight, standardized channels such as URLs (Jackson, C.)
Shared conventions: agree on a shared design system, code style, and configuration
Adopting a micro frontend architecture is not without tradeoffs. Costs include:
Duplication of dependencies, increasing the number of bytes users download (Jackson, C.)
Increased fragmentation between teams as a result of increased autonomy (Jackson, C.)
Rather than diving into a how-to plan for implementation, let’s take a step back to examine the benefits of adopting micro frontends. We derive these benefits from breaking the application into smaller pieces and allowing teams to evolve independently without excessive coordination.
For existing, monolithic frontends, motivations to move to micro frontends include being held back by yesteryear’s tech stack and maintaining a poor-quality codebase. To avoid the risks associated with a full rewrite, we begin strangling the monolith piece by piece (Jackson, C.). We accomplish this by delivering new features via micro frontends. As we introduce new features and replace existing ones, the responsibility of the monolith decreases over time. Eventually we arrive at a set of stable features in the monolith or retire it altogether.
Having proven the ability to introduce new features without touching the monolith, the team becomes capable of incrementally upgrading the user experience (Jackson, C.). Under a monolithic structure, upgrading to a new version of a framework was a significant undertaking. Now, rather than stopping feature development to update everything at once, we update each micro frontend to the latest version as it provides benefit to the organization.
The decoupled nature of micro frontends encourages experimenting with new technologies. New technologies and approaches to problem solving provide the business with competitive advantages. Implementing successful experiments in a monolithic environment is difficult due to tight coupling; often we cannot implement an experiment without affecting much or all of the application. With micro frontends, we mitigate risk and ease implementation through the ability to perform and adopt experiments in an isolated fashion.
Smaller, Decoupled Codebases
By definition, the codebases of individual micro frontends are smaller than that of a monolithic frontend (Jackson, C.). Smaller codebases tend to reduce cognitive load, or the amount of working memory required to understand the code. Reduced cognitive load enables new and existing team members to more easily understand the codebase, enhancing productivity.
Less code per codebase results in faster development cycles. Developers benefit not only from reduced cognitive load, but also from decreased time to build, lint, and test new code. When developers fire up the test server, there is less code to compile, allowing the system to start more quickly. Linters checking for code convention and style take less time to examine the codebase. And test runners have fewer tests to run and less code to examine when calculating code coverage.
A smaller codebase also reduces the opportunity for coupling independent components. While it’s possible to write coupled code in a micro frontend architecture, organizing code around bounded contexts of the domain model reduces its likelihood (Jackson, C.). For example, if the networking domain exists in a separate codebase from the storage domain, it becomes more difficult to unnecessarily couple them.
Independently deployable micro frontends reduce the scope of a given deployment. Reduced scope results in reduced risk. Just as with microservices on the backend, each micro frontend should have its own continuous delivery pipeline to build, test, and deploy code into production. When a micro frontend is ready to go into production, the decision to do so is up to the team who builds and maintains it without the need to coordinate across development teams (Jackson, C.).
By decoupling codebases and release cycles, we can move towards fully independent teams who own segments of the product. We divide the teams based on vertical slices of functionality. In frontends, it’s natural to organize teams around what end users see (Jackson, C.). Each domain, view, or sub-view may constitute its own micro frontend depending on the size and complexity of the interface.
As mentioned in the introduction, adopting micro frontends is not without costs. When compared to monolithic frontends, the most significant tradeoffs are increased total payload size; fragmentation of the technical stack; and potential fragmentation of user experience. We will not attempt to mitigate the cost of increased total payload size but will discuss strategies for mitigating fragmentation.
As a result of building micro frontends independently, shared dependencies will not be cached by our users. Even if two micro frontends depend on the same version of Angular, if we bundle Angular with non-shared dependencies or serve bundles from different URIs, users’ browsers will re-download the shared dependency for each micro frontend. This results in increased total payload size when viewed in the context of the entire application.
We could eliminate duplicate payload content by externalizing common dependencies into bundles accessible from a single URI. However, this couples the runtime of two or more micro frontends by dictating each must utilize the same dependency versions. If a new version of a dependency introduces breaking changes, we’ll be forced to coordinate an upgrade between all teams that share the dependency. These are some of the very efforts we’re trying to avoid by adopting micro frontends.
Doing nothing about duplicate dependencies may be the best approach. In fact, each micro frontend may still load faster than if we took a monolithic approach. Without code splitting and lazy loading, a monolithic frontend downloads the entire application at once. By loading a single micro frontend, the user downloads a fraction of the total application code. How small a fraction depends on the size of shared dependencies relative to a given micro frontend’s unique code.
For applications built with micro frontends whose shared dependencies are limited in size, accepting duplicate downloading of dependencies is a valid approach. Along the same lines, for environments in which bandwidth and latency are not a bottleneck, limiting payload size may not be of critical importance. If we do choose to set up shared dependency bundles, we must ensure optimizing for payload size outweighs the cost of coupling runtimes across teams and upgrading micro frontends in lockstep.
How do we compose the output of autonomous teams into a cohesive whole? Shared methodologies reduce fragmentation while allowing teams to operate independently. Large organizations like Salesforce and Google produce a consistent user experience by implementing a shared design system (Fanguy, W.). Software teams employ tools like code linters and shared configuration to ensure consistent style and quality. These common threads mitigate the risk of autonomous teams creating disjointed user and developer experiences.
A design system starts with clear standards that guide building reusable components. Next, we assemble the components together to build a variety of applications (Fanguy, W.). The development of standards is more than defining size and color; it includes use cases that define why and how to use a particular component.
According to Marco Suarez, a product designer at InVision, “understanding not only the what, but the why behind the design of a system is critical to creating an exceptional user experience. Defining and adhering to standards is how we create that understanding.”
Atomic Web Design
In his blog post titled atomic design, Brad Frost describes an interface as the composition of smaller components. In chemistry, atoms are the fundamental component of all matter. Using the analogy of chemistry, Frost breaks down an interface into its fundamental building blocks. We combine these building blocks to build up a design system. In the atomic design system, we comprise an interface from atoms, molecules, organisms, templates, and pages.
Atoms are the basic building blocks of matter. In the context of web interfaces, HTML tags, color palettes, fonts, and animations constitute the atoms (Frost, B.). On their own, atoms do not provide much value. By combining them together, we take the first step towards building an interface.
Atoms combine to form molecules. A label, input, or button isn’t especially useful by itself but, in combination with other atoms, it becomes part of a molecule capable of capturing user input. In the atomic design system, molecules are relatively simple combinations of atoms intended for reuse (Frost, B.). Molecules form the basis of component libraries.
Groups of molecules form an organism: a relatively complex, distinct section of an interface (Frost, B.). Organisms may consist of similar or disparate molecules.
Frost provides the example of a masthead organism. The masthead organism sits at the top of a digital newspaper. The masthead consists of diverse molecules including a logo, navigation menu, search form, and list of social media channels.
While the masthead organism consists of disparate molecules, the product grid organism consists of multiple copies of the same molecule. The product molecule presents a product’s image, title, and price. The product grid organism repeats the product molecule in a grid layout across the interface.
To define the next level of abstraction, Frost breaks away from the chemistry analogy to define the template. A template is a group of organisms we stitch together. Templates begin their lives as wireframes and increase in fidelity over time to become a final deliverable (Frost, B.). A template brings together a group of organisms to accomplish a specific task.
The highest level of abstraction in Frost’s system are pages. Pages are instances of templates (Frost, B.). Imagine the template as a stamp; we get a page when we dip the template in ink and place it on paper.
Pages are where a design system’s rubber meets the road. Pages put the design system’s components in context, exposing whether our molecules, organisms, and templates address the problems we set out to solve. What happens when a headline exceeds the typical number of characters? Does the template work when we have 10 products? What about 10,000? If the result is undesirable, we loop back and rework pieces of the design system.
Steps To Create A Design System
InVision creates a design system by following these steps (Fanguy, W.):
Conduct a visual audit: review the current design, taking stock of the visual qualities of elements
Create a visual design language:
a. Color: 1-3 primaries that represent the brand with a range of tints and shades to provide a few more options
b. Typography: one font for headings and body copy and one monospace font for code
c. Sizing and spacing: scale elements and fonts relative to one another in a way that produces rhythm and balance
d. Imagery: guidelines for illustrations and icons along with file formats
Create a UI library: unlike the visual audit where we examined the visual qualities of the design, this step looks at the actual components that make up the interface. We enumerate every button, form, modal, and image. Having taken stock, we merge and remove what isn’t needed
Document each component’s functionality and when to use it: defining use cases for each component is what separates a design system from a component library
Code Style & Configuration
Code linters enforce consistent format and conventions across codebases. They save time in code reviews because a linter automatically fix code that violates style rules and notify the developer of cases it can’t fix. A popular linter is Prettier, a tool that provides configurable, opinionated code formatting across many languages.
Having independent teams does not mean accepting vast differences in code quality. Test runners offer configurable minimum coverage standards for code no matter what framework or version of a framework the team uses. Tools like Karma and Jest allow us to set minimum test coverage for statements, branches, functions, and lines of code.
Source Control & Continuous Integration Hooks
Code style and test coverage rules are only as good as their enforcement. By consistently executing our linters and test runners whenever developers commit and push code, we ensure enforcement of code style and quality.
Why Micro Frontends?
The growing complexity of frontend applications necessitates a more scalable architecture. Our people need manageable, effective strategies for upgrading and replacing pieces of frontend functionality. We should have the ability to experiment with and adopt new features without requiring changes to the entire application. Micro frontends provide a technique for satisfying these needs while also delivering a consistent user experience.