Scaling Teams with Micro Frontends
Monoliths are problematic anywhere in the stack. Splitting up big monolithic applications into microservices is a hot topic for Backend services, but huge Frontends exist and have their fair share of problems.
A monolithic Frontend app might be quicker to build and easier to release, compared to a Backend app. However, things fall apart when multiple teams with different deadlines and goals end up working on it. Microapps are to the Frontend what microservices are to the Backend: a way for teams to gain back autonomy and speed of release.
Split apps by url
In order to split the monolith,
you need to figure out which bits of the application are owned by the different teams.
One rule of thumb is that a single microapp should respond only to requests at a specific
URL prefix. An app handling payments will be concerned with anything under
/payment such as
/payment/view/a123bc. Avoid separate top-level URLs
/view-payment as they're trickier to route.
A router in front
For Backend services, an Api Gateway receives all traffic coming through and routes requests to the correct microservices. The same technique can be employed on the Frontend by using something as simple as an Nginx proxy. There are great options available for Kubernetes, such as Traefik and Skipper, but be sure to understand all the complexity involved with those solutions.
I'm a big fan of Backends for Frontends, a less painful and perhaps more romantic approach to routing. You can use the same backend you already have to route requests, there's no need for Nginx or any fancy proxy in front. It very much depends on how much traffic you get, but chances are you'll be able to handle moderate load without issues.
Use URL prefixes
As said, apps should own a single top-level URL. This is so that the router is
simple to configure and the system easier to reason about. In the Payment app
example above, you wouldn't have to list all the actual URLs the app is able to
handle. That would be a nightmare. A more robust approach is to have the router
route traffic based on URL prefixes. Anything that starts with
/payment will be
forwarded to the Payment app.
The addition or removal of a route won't require a reconfiguration of the router, as long as the prefix stays the same. Because of this, rollbacks are easier to handle as the router won't be involved. One less thing to worry about.
Changes to the router configuration should be seen as database migrations: they are backward compatible and don't cause disruption when applied to a running system. Always allow for a transition period with two routes co-existing (old and new) instead of dropping the old one and introducing the new one at the same time. The old route can always be removed later. Negative diff commits are great — consider however the slower approach of deprecating a route first and make sure it is effectively not receiving any traffic (perhaps by a link that was missed) before removing the relevant code.
Fallback app and subdomains
Pick one of your apps to act as a fallback app. Requests that can't be
handled by any other app are going to be served by this catch-all app.
Certainly any unknown request is going to be a
404, so why bother?
Chances are there's a marketing department in your company or perhaps an entire team
focused on growth. These folks need to come up with strong URLs for SEO and
might want to experiment and change them often. Updating the router every time is
going to be unfeasible. A better option is to have the marketing site being the
catch-all app, so that the router doesn't have to know any of the actual routes.
If you can't roll out a router, consider using a subdomain. There are many
companies that serve traffic for logged in users at
anonymous traffic goes through
www.company.com. You can also do the
about.company.com being the main website. Although the marketing
department will hate you for that.
Going with microapps might look like an architectural defeat. Components in some flavour (be it React components, web components or what have you) should be enough to scale an application. Even so, a large application will still be challenging to work on as it grows in size. When struggling with deploy times, consistently broken builds and slow release cycles, microapps could be the cure. As with microservices, you have to be careful. Having your users download a 3mb bundle for each different page is not what I'm advocating for. Teams need to agree on a single framework and be mindful of user experience. Bundle splitting plays a big role here — users should download React only once.
There's one thing I absolutely love about microapps: static pages. It seems like everything nowadays has to be a Single Page Application, otherwise you're missing out. I think SPAs are great for highly interactive pages. Go and build those pages as SPAs, but anything else can be statically generated or even server rendered (a tiny node/ruby/python app can go a long way). As most things in technology, reach for the simplest abstraction first and see if you can solve the problem with that. Microapps allow teams to work at different abstraction levels. When you need the big hammer (SPAs) then you can and should go for it. But when you realize a large monolithic React app is no fun to work on, then consider splitting it up into micro frontend applications.