Microservices have exploded in popularity in the past decade, with many organizations attempting to use this architectural approach to avoid the limitations of large, monolithic systems. While much has been written about using this approach to decompose the backend of enterprise software applications, many companies continue to struggle with how to manage the frontend.
Whether you want to integrate new features into an existing web application or are trying to scale your development so that multiple teams can work on a single product simultaneously, these are real world problems that can negatively impact your ability to deliver high quality outcomes to your customers.
By focusing on the architecture of complex modern web applications we can employ patterns for decomposing the application frontend into smaller, more loosely coupled parts that can be developed, tested and deployed independently. This can be achieved while still delivering a single cohesive product to clients. This technique is referred to as "micro frontends" and is generally defined as "an architectural style where independently deliverable frontend applications are composed into a greater whole".
There are numerous benefits for this technique, and it should be no surprise that these advantages are nearly identical to the ones promoted for microservices. They include:
- smaller more maintainable codebases
- decoupled autonomous deployments and teams
- ability to develop in a more incremental fashion
Let's explore these benefits in more detail...
Maintainable Codebases
The source code for each individual micro frontend is much smaller than the source code of a single monolithic frontend. Smaller codebases tend to be simpler and easier for developers to manage. In particular, the complexity arising from unintentional and inappropriate coupling between components can be avoided. In addition, the "inner development loop" (the highly iterative process a developer follows when writing code, compiling, unit testing, committing to source control, and deploying to an environment) is significantly reduced, resulting in higher productivity.
Independent Deployment
Similar to microservices, a key attribute of micro frontends is independent deployment. This reduces the scope of any given deployment, which in turn reduces the associated risk. Each micro frontend should be part of a continuous delivery pipeline, which builds, tests and deploys it all the way to production. Decoupling codebases and release cycles allows teams to operate independently, reducing the need for constant coordination, and providing full ownership of everything they need to deliver value to customers. For this to work effectively, teams need to be formed around vertical slices of business functionality, rather than around horizontal technical capabilities.
Incremental Upgrades
Micro frontends provide the freedom to make incremental updates to the product features, architecture, dependencies, and user experience. In addition, if there is a need to experiment with new technology, or new modes of interaction, it can be done in a more isolated manner than ever before.
Micro Frontends in Blazor
A single page application (SPA) architecture is well suited to micro frontends as generally there is a single container application, which is responsible for:
- rendering common page elements such as headers and footers
- addressing cross-cutting concerns like authorization and navigation
- combining the various frontend UIs together onto a page
Blazor is an open source and cross-platform web UI framework from Microsoft for building single-page applications using .NET and C#. Oqtane is an open source modular application framework for Blazor which enables micro frontend development.
Composition
A Blazor application is bootstrapped by a single Razor page which loads the initial container application and initializes the router. The router is the "brain" of the application, maintaining the essential elements of application state and coordinating the interaction between components. The router is responsible for loading metadata that is used to determine which Razor components should be rendered on a logical page.
Integration
Similar to other .NET features, Blazor defaults to a build-time integration approach for deployment. This essentially combines all dependencies into a single deployable package. However, this approach means that the entire application needs to be assembled and released in order to make a change to any individual part of the product. Obviously this negates one of the key benefits of a micro frontend approach, so this is NOT a viable approach.
Having gone to all of the trouble of dividing the application into discrete codebases that can be developed and tested independently, it is best to not re-introduce all of the coupling at the release stage. Instead it is preferable to integrate the frontends at run-time rather than at build-time. This is where the capabilities of Oqtane are especially relevant as it allows Razor components to be independently deployed and dynamically loaded and instantiated at run-time to produce a composite user interface.
Components
Micro frontends are developed as Razor components which are packaged as Razor Class Libraries (RCLs). These Razor components are dynamically loaded and injected into the page based on run-time parameters. These components rely on the standard Razor component life cycle events and can support multiple Blazor hosting models. Razor Class Libraries (RCLs) are bundled as Nuget packages which can be deployed as part of a standard build pipeline or at run-time.
Styling
CSS is inherently global, inheriting, and cascading which creates challenges in a micro frontend environment where styling integration is essential. In order to ensure consistency across your Razor components it is best to adopt a foundational CSS library for shared styles, while also encouraging developers to create their custom styles using techniques which ensure they will behave predictable and will not clash with one another when composed together into a single application.
Component Libraries
As mentioned previously, visual consistency is important across micro frontends. A common approach to resolve this problem is to provide a library of shared, re-usable UI components to developers. The main benefits of creating such a library are reduced effort through re-use of code, as well as visual consistency. In addition, your component library can serve as a living styleguide, and it can be a great point of collaboration between developers and designers.
The most obvious candidates for sharing are visual primitives such as icons, labels, and buttons. More complex components such as a sortable, filterable, paginated grid may also make sense. However, it is important to ensure that your shared components contain only UI logic, and no business or domain logic. When domain logic is put into a shared library it creates a high degree of coupling across applications, which is contrary to the goals of micro frontends.
The other challenge with a shared component library, is related to ownership and governance. The best model is to appoint a custodian that is responsible for ensuring the quality, consistency, and validity of those contributions. The job of maintaining the shared library requires strong technical skills, but also the social skills necessary to cultivate collaboration across many teams. The Blazor ecosystem offers a plethora of third party shared component library options - both open source and commercial.
Communication
One of the most common questions regarding micro frontends is how to allow them talk to one other. In general, the recommendation is to minimize the communication between them, as this often introduces the sort of inappropriate coupling that we're seeking to avoid in the first place.
That being said, some level of cross-app communication is often needed. The Blazor router is a good option for communicating across logical page boundaries. INotifyPropertyChanged events are good options to communication between Blazor components on the same logical page. The most important thing is to think long and hard about what sort of coupling you're introducing, and how you'll maintain that contract over time. Just as with integration between microservices, you won't be able to make breaking changes to your integrations without having a coordinated upgrade process across different applications and teams.
Backends
A frontend is generally not very useful without a backend. One of the value propositions of Blazor is that you can have full-stack C# teams, who own the application development from UI all the way through to API development, and database and infrastructure code. Taking a feature-based approach to software development as opposed to a layered approach, allows you to deliver a fully functional and integrated feature from top to bottom. Some folks like to refer to this approach as "vertical slice" architecture.
One pattern that helps is the Backend for Frontend (BFF) pattern, where each frontend application has a corresponding backend whose purpose is solely to serve the needs of that frontend. While the BFF pattern might originally have meant dedicated backends for each frontend channel (web, mobile, etc), it can easily be extended to mean a backend for each micro frontend.
Authentication
Another common question is how should the user of a micro frontend application be authenticated and authorized? Obviously customers should only have to authenticate themselves once, so this usually falls firmly in the category of cross-cutting concerns that should be owned by the container application. The main Blazor application provides the middleware capabilities for authentication and authorization.
Testing
There is not much difference between monolithic frontends and micro frontends when it comes to testing. In general, whatever strategies you are using to test a monolithic frontend can be reproduced across each individual micro frontend. That is, each micro frontend should have its own comprehensive suite of automated tests that ensure the quality and correctness of the code.
Conclusion
As enterprise applications continue to get more complex, there is a growing need for more scalable architectures. Therefore it is important to be able to draw clear boundaries that establish the right levels of coupling and cohesion between various aspects of a system. A micro frontend approach makes it possible to scale software delivery across independent, autonomous teams.