Decoupled deployments, splitting strategies and trade-offs
This week let’s look into an interesting thread about microfrontends that happened on Twitter, it all started with:
This sparked a lot of different answers, many just assuming that microfrontends are a buzzword or inherently bad, but if you’re really interested in learning the subject the question that usually follows is:
I tried to give my own point of view on the subject:
This is, in my opinion, the main focus of a microfrontends architecture, componentization inside a monorepo allows isolating complexity and makes reusability easier, but it doesn’t decouple deployments.
When working in a monorepo the entire application needs to be built, tested and deployed even for a small bug fix. This depending on your pipeline can take its time, not to mention that you may need to deal with things like code freezes, and either merge queues or release events with multiple changes from multiple teams.
This can be seen purely as a deployment pipeline issue which we can improve by using advanced tooling such as TurboRepo. However, this doesn’t change the fact that the entire application needs to be deployed and the risk associated with it.
Looking at this as an architectural issue we can say that by breaking the system into separately deployed applications, then a change for any given service would be scoped to that service only. This then becomes an issue of how to split frontend applications.
There are two main strategies for splitting frontend applications which are usually known as horizontal and vertical. However, these strategies come with very different trade-offs and have very different implementation complexity.
Vertical
In a vertical split, an application is split into smaller fully self-contained applications.
A team can own a specific view, or group of views, allowing them to operate with full autonomy in their assigned area of the application.
This keeps development closer to the traditional way of building applications.
However in this strategy, each domain can still get pretty large, and there’s a performance cost when switching between applications.
Also, you may need a strategy to have a design system to be shared between these applications and you’ll have to assume eventual consistency between them as they get deployed with newer versions.
Vercel provides a good example of this strategy and its associated trade-offs
Horizontal
In a horizontal strategy, each view can be split into multiple independent applications and each of these applications can be loaded across many views. This means that no single team owns the full view output, it’s a result of the composition of multiple independent applications.
This strategy allows for smaller applications however it also brings more complexity in terms of composing the final page and coordinating the multiple parts to act as a whole.
In this strategy you need to at least consider:
How to communicate between applications,
A way of isolating applications from each other
And due to the constraints of the browser and networks, you’ll probably need to figure out how to share code between these applications,
Many of these things are not clear how to achieve and may impose significant trade-offs in your overall architecture
Isolation
When thinking about isolating applications from each other there is no browser capability available currently that doesn’t impose significant trade-offs.
You’ll have to deal with browser globals, CSS, HTML, and Javascript.
The most powerful isolation capability provided with cross-browser support is iframes. However iframes pose some significant constraints in how the applications are loaded and may come with some accessibility issues.
Then you have web-components that allow for good CSS isolation but currently don’t support server-side rendering.
Or you can resort to conventions and tooling to scope CSS
Shared Code
In a horizontal strategy as the number of applications on the same page increases so does the bundle size, and at some point, it becomes a problem.
This is where the proponents of module federation come in, there’s a lot of promotion of module federation in the microfrontends space.
However i believe there are very few clear trade-off analyses about this strategy. The way i see it when using module federation you’re trading a build time integration for a runtime integration for your dependencies.
At runtime loading, different versions of shared dependencies than the ones you’ve tested an application with can be a significant risk. You can try to use semver to check if the versions are compatible but semver is just a social contract there are no guarantees.
This in my opinion can be a significant risk and in the next issues i plan to do a more detailed analysis of the trade-offs of using module federation and the alternative strategies that can be used
Interesting links:
A solid example of microfrontends with a vertical splitting strategy
https://vercel.com/templates/next.js/microfrontends
A good outline of issues that can happen with microfrontends that can be helpful when creating your own strategies
https://medium.com/swlh/problems-with-micro-frontends-8a8fc32a7d58