Micro-services…on the front-end?
I first found out about “micro-frontends” from the ThoughtWorks Tech Radar—while the concept of building decoupled applications and stitching them together isn’t novel, the term micro-frontends has been getting quite a bit of buzz lately.
This article was first published as exclusive content in partnership with Morning Cup of Coding. Please head over and subscribe to Pek’s newletter, he’s curating awesome content every week!
In short, micro-frontends is what happens when we take a lot of the same ideas behind backend microservices and adapt them for client-side development.
Oftentimes, when companies adopt a microservice architecture on the backend, they leave their front-end apps as monoliths—after all, that’s how they’re shipped to the user, aren’t they? This common architecture looks something like this:
However, this isn’t truly an end-to-end microservices architecture. While the backend is split up according to business requirements, the frontend is still all in one app. Over time, problems can begin to present themselves, especially if your microservices & UI demands go hand in hand (as they often do!).
With a micro-frontends architecture, we can split our entire application by business domain across the entire stack! This enables your front-end teams the same level of flexibility, testability, & velocity that your backend teams are getting from their microservices. On a high level, using a micro-services looks more like this:
Are micro-frontends the right choice for you?
There’s a lot of great reasons to adopt a micro-frontends architecture, but they do add a lot of complexity to your front-end ecosystem. Before you go ahead and modify your app to use micro-frontends, make sure that you take a moment to evaluate if the complexity is worth the payoffs that micro-frontends give you. Here’s a couple reasons why you might adopt a micro-frontends architecture.
- You have a huge app with a ton of complexity. Chances are, most of us aren’t working at companies that apps big enough to warrant micro-frontends (I know I’m not!). Micro-frontends really start to shine when your front-end app has grown so huge that no team—let alone one person— can understand how the entire thing works. Don’t jump on the micro-frontends bandwagon if your app isn’t at this scale yet, the added complexity may hurt you more than it helps.
- You have multiple teams contributing to your app, and each has their own deployment cycles. Once again, a problem that only starts to surface when you’re operating at a large scale. Having each piece of the larger application separated into multiple micro-frontends allows each team to deploy their piece of the product without interfering with the release cycles of the rest of the organization.
- You’re itching to learn something new on a side project. Side projects are the perfect place to play around with new architectures, technologies, even if they’re overkill. If you over-engineer a side project it’s not gonna hurt your business like if you over-engineer your company’s app. For what it’s worth, most of my experience with micro-frontends has been through side-projects, since my company isn’t currently set up with a micro-frontends architecture. If you want to play around with micro-frontends because you’ve been hearing some buzz about them but you don’t have a business reason at work to use them, whip up a side project with a few micro-frontends in it & see how you feel about them.
What’s unique to micro-frontends?
For the purposes of the rest of this article, we’ll be playing around with some actual micro-frontends I whipped up to show some of these principles.
And here’s a link to the GitHub repo. All of the sample code used in this app should be here. Please feel free to poke around and let me know if there’s any issues—I’d love to make this as awesome as possible!
When you inspect the app, you’ll see it’s not too elaborate. One micro-frontend is a counter, and the other 2 micro-frontends increment the counter.
I find it helpful to make something super simple when learning about more high-level architecture stuff like this. That way we can focus on things like the actual architecture of the app rather than rolling 1000+ lines of authentication logic or another todo list. Since our “business logic” is pretty lean we can spend most of our time focusing on the architecture patterns.
For the purposes of this article I’ll keep referring back to these example micro-frontends. This example isn’t necessarily inclusive of everything that can be done in micro-frontends—to do that we’d need a book!
As we dive more into the micro-frontends architecture we’ll start to see some common things that micro-frontends share with backend micro-services and some key ways that they are different.
Micro-frontends are isolated from each other
Although the term micro-frontends is flashy & they invoke the association with large organizations, at their core micro-frontends are a mechanism for isolating code.
Since isolation is at the heart of micro-frontends extra care is needed to make sure that code from one micro-frontend doesn’t bleed into another micro-frontend. For example, if you had micro-frontends with the following CSS on the same page you’d be in for a world of trouble.
In this case, each micro-frontend is grabbing the same CSS selector, & we’re gonna have clashes. However, having each team write their code in isolation increases the potential for namespace clashes—after all, CSS is a global namespace.
One easy solution would be to have each team choose a namespace under which they will place all of their “global variables”, allowing multiple teams to put stuff in the global scope without clashing.
As long as each team puts all of their CSS inside their namespace they’re fine. However, we’re all human, and we know humans make mistakes. The problem of global CSS can be automated away using something like CSS Modules or a CSS-in-JS solution where all of the CSS classes recieve a hash at the end to preserve their uniqueness. So developers can use the simple class
cta-button inside of their own CSS, and it will be transformed into
cta-button__5f63ade at build time. Furthermore, you could even add the team prefixes in a build step if you’d like—I know PostCSS has a plugin to do this automagically.
Another good way to make sure that variable scope doesn’t bleed into another mini-app is packaging your micro-frontends as HTML custom elements (web-components!). The shadow-dom is gonna make sure that none of your CSS bleeds outside of the micro-frontend. In the case of our example app all of the micro-frontends are implemented as web-components, with a simple server stitching them all together before sending them to the user.
Here’s a quick snippet of one of the micro-frontends.
Micro-frontends are split by business domain, not by location on the page
Another big benefit of adopting micro-frontends as an architecture is that it forces you to think about your website as various business requirements rather than a collection of items on a page.
Let’s say we have an e-commerce site. Your mind might instantly start to break apart the various functionality of what the site needs to do:
- There should be a way to shop for items
- There should be a checkout “cart”
- There should be a way to see order history
See what happened? Even without any designs we have already identified 3 separate parts of the application that can be developed in isolation from each other. Furthermore, each micro-frontend is hyper-focused at solving one problem, which will definitely help our code stay clean over the lifetime of the app.
Of course, these micro-frontends will eventually need to be stitched together into a single page, but dividing our micro-services by business domain rather than page design allows us to quickly set up alternate flows or versions of the website. If the checkout cart is on the right side of the header, we can easily move that CTA anywhere on the page we want without having to worry about data-flow or CSS inheritance problems.
Micro-frontends are independently deployable
One of the key benefits of isolating team code into separate micro-frontends is that it makes independent deployments of each service a possibility. Let me be clear—adopting a micro-services approach for your front-end doesn’t magically mean you can deploy them separately. You have to set them up in a way that allows each one to be versioned and released without affecting the other services.
First off, this means we need the isolation of our services mentioned above. If your services aren’t isolated & they’re intertwined & tangled up, you’re not going to be able to deploy them independently.
However, the potential for real independent deployments is one of the key benefits that I see to micro-frontends. If you’re just looking for isolation of team code, you can do that with properly splitting out components & distributing your code as packages. You still use web components if you’d like to isolate team code. But where micro-frontends really shine is when you can deploy updates to micro-frontend “a” without having to re-deploy micro-frontend “b” or “c”. This means if the teams working on micro-frontend “b” aren’t quite ready to ship their part of the product, the team behind micro-frontend “a” can release whenever they are ready to go live.
How do we set up our micro-frontends so that they can be separately deployed? One common solution that I found was to set up a small orchestration layer that serves the action
index.html to the end user. The stitching layer is responsible for grabbing all the micro-frontends from their respective deployments & gluing them all together onto the page. This would also be the place to implement things like global layouts. With the stitching layer our application architecture looks like this.
When we dive into the code for the stitching layer, we’ll find that it’s not actually too intense. I chose to grab the code from the separate micro-frontends through HTML imports since I’m using web components, but you could also easily export a JS bundle and stitch them together with a server-side templating library.
Note: HTML imports aren’t very well-supported yet across browsers. If you choose to go with them you’ll likely need to ship with a polyfill.
Here’s what the server looks like. You’ll notice it’s pretty lean, not a ton of stuff going on here.
Most of the actual stitching logic is inside of the
index.html file being sent to the user. Since we’re currently using HTML imports to stitch the micro-front-ends together, you’ll see that the
index.html is also pretty small. The cool thing about stitching our index.html together this way is that as long as we declare a web-component matching each micro-frontend on the imported routes we can change whatever we want inside each micro-frontend.
If you’re stitching your micro-frontends together on the server and you’re not using web-components, you might want to look into having your stitching layer server-render your micro-frontends. This is huge if you need search engines to see your site better or you’re trying to optimize the amount of bytes sent to your end users.
Micro-frontends should still feel like a single application
I know, this part feels contradictory, doesn’t it? However, first and foremost your app should feel cohesive, regardless of your architecture. Your end users don’t care whether your app is a monolith or micro-frontends—they just want an app that loads fast and solves their problems (whatever problem you’re solving in your app). Granted, something like micro-frontends might help you meet that goal in a more scalable way, but at the end of the day the user should feel like they are using a single application.
How can we manage this, given that each team’s code is completely isolated? It would be really easy to end up with slightly different look & feel on each micro-frontend.
It turns out the issue of brand consistency isn’t a new issue or unique to micro-frontends. One common way to go at this is through adopting a design systems approach—here’s a great list of a ton of awesome design systems used by a lot of big companies. If you’re doing micro-frontends, it may be a good idea to implement a design system so that regardless of team code isolation, each micro-frontend has the same look & feel to the end user.
If you’re using web components already to stitch your micro-frontends together, you could also standardize your UI styles by creating a library of web components. This allows you to use the same component library regardless of what technologies your micro-frontends are using. Which brings us to our next point…
Each micro-frontend can have its own tech stack
Just like developing backend micro-services allows you to write one server in Go, another server in Python, and yet another with Node, splitting your frontend architecture into micro-frontends allows each micro-frontend to have its own technology stack.
Now, just because you can use multiple frameworks doesn’t mean you should. I’m sure by now some of you may be thinking “isn’t sending down React and Vue” gonna negatively impact my users?” After all, on the client we’re very constrained by the network connection and how much code we’re sending to the users.
The short answer to this is, yes, shipping multiple technology stacks in micro-frameworks has the potential to negatively impact your users. Running your app with React, Angular, and Vue all at the same time could produce some serious rendering jank on a low-end mobile device, not to mention what it’s gonna do to your page load times (last time I checked, loading 3 JS frameworks isn’t the best way to get speedy critical renders).
However, there’s other facets to whether or not shipping multiple frameworks & technologies is a good choice for you. Maybe your old code is written in Angular 1 (or jQuery!) & you want to move your company’s products to React. Or maybe you really want to future-proof your current Vue app for whatever the next hot technology is in 5 years. Having a micro-frontends architecture may make it cleaner for you migrate chunks of your app at a time. Each team can work on migrating and since the apps are already decoupled from each other you don’t have to worry about getting your shiny new code tangled up in your legacy code.
Another side that I’ve seen advocated for is to simply have one common stack that you use across your micro-frontends. For example, you can write each micro-frontend in a single framework, React for example. Even with this limitation there’s still a lot of choices to make in tech stack.
- TypeScript / Flow? ReasonML / PureScript? No types at all? CoffeeScript (lol)? Each team can choose what’s best for them, since at the end of the day it all compiles down to JS. Of course there will be a learning curve, so if developers are jumping between all the micro-frontends might be better to standardize these types of decisions.
- How does each micro-frontend handle state management? One micro-app might be complex enough that it needs a state management library, another micro-app might be able to get away with raw component state.
- SASS / LESS / PostCSS? CSS-in-JS? (If you go the CSS-in-JS route I recommend doing the same thing as frameworks and sticking to one across all micro-frontends)
Front-end development has become more and more reliant on transpilation & compile steps, so anything that supports the JS / HTML / CSS compile target is fair game for allowing each micro-frontend to choose its own tech stack without negatively impacting bundle load. Of course, having a myriad of technologies across your front-end ecosystem increases the complexity if you have developers jumping between micro-frontends, so this is best suited for scenarios where a team decides on a tech stack for the codebase that they will be the owners of.
Micro-frontends can still communicate with each other
I thought we said that micro-frontends are supposed to be isolated, aren’t they? However, in real-world applications, sometimes one micro-frontend will need to communicate with another micro-frontend. For example, imagine you have one micro-frontend owned by team “shopping”, and when a user clicks on a shopping result they get the item added to their “cart”. The cart is a separate micro-frontend, managed by a completely separate team.
In our example, micro-frontend A & micro-frontend C can both influence micro-frontend B. The simplest way to do this is to remember our name-spacing concerns that we mentioned earlier & create some Custom Events. Since these events are available on the global
window object, they’re highly transportable, and the namespace for each micro-frontend’s events makes sure they don’t accidentally collide.
Making micro-frontend A & C emit namespaced events doesn’t require us to add too much to our micro-frontend. Here’s the event emitter in micro-frontend A.
And in micro-frontend B we can simply listen for this event & respond to it.
Granted, making our micro-frontends communicate with each other greatly increases the potential for tight coupling. Think long and hard before you go and connect micro-frontends together, since this decreases the ability to independently deploy changes to each one. However, when you do need to connect two domains of your business (and you will need to at some point!), my advice would be to opt for natively supported DOM APIs rather than creating your own custom event emitter/subscriptions.
The whole point of using micro-frontends is to make sure that each app is disconnected from the environment it’s deployed in, and rolling your own emitter will increase each micro-frontend’s dependency on certain things being available in the global scope.
I hope this dive into micro-frontends was helpful for you. I know I’ve had a blast writing this & learning about this architecture.
One of the biggest takeaways I’ve gotten from this experiment is that micro-frontends are a huge headache. The increased complexity over a monolith application is no joke—I had to run each micro-frontend separately until I wrote a script to run them all at once. Even that is added complexity over the simple
npm run dev that you get with your standard monolithic frontend.
In addition, any time breaking changes to a micro-frontend that affects another micro-frontend both have to be deployed simultaneously. In the early stages of development this can slow down your speed of development. I found this to be especially painful when I was still figuring out what I wanted each front-end to do, since they were changing rather frequently.
However, deploying new code to a single micro-frontend was a joyous experience. I was able to achieve independent deployments of each micro-frontend fairly early in my development process. Of course, in a bigger app this could be more complex, but this architecture really allows each micro-frontend to progress at its own pace. This could be extremely useful if you already have a bunch of teams split up by business domain and all of them have their own roadmaps.
If you liked what you read, please head on over to my Medium profile & follow me! I try to get articles like this one out semi-regularly, and I’d love to make sure that you hear about them when I do publish them. I’m also on Twitter (@benjamminj), please tweet at me and let me know what you thought and what intrigues you about micro-frontends! I’d love to chat!