Historically, backend development occurred in the form of giant monolithic blocks of code. As a business' requirements changed or the scope of their product increased, engineers would add code to a single ever-growing backend application. As the Internet matured and more users began to use web apps, large companies like Netflix started to run into scaling issues with the monolithic approach to software development.
Netflix and others were finding that their monolithic applications took a long time to build and release to customers, and that over time developer velocity decreased substantially as the complexity of the monolith grew. Microservices were the answer to this: a method of software development where backend systems are broken down into small, distinct, and loosely-coupled services.
With microservices, individual teams could work on their own microservice(s) in isolation from the rest of the engineering organization which–in theory–boosted developer velocity and made it far easier to release updates more often. Microservices worked well for Netflix, and today virtually all large technology companies are using microservices to some extent.
Like all architectural decisions, microservices are not a panacea and come with tradeoffs. Matt Ranney's talk "What I Wish I Had Known Before Scaling Uber to 1000 Services" is simply one of the best talks available for understanding the dangers of blindly adopting a microservices-centric architecture, and is–in my opinion–required listening for anyone considering their implementation.
One of the big takeaways from his talk is that the decision to adopt microservices in their organization should be a practical one. One should only start breaking out microservices once they are encountering actual problems with their existing implementation, and when they have time to do microservices 'properly.'
"What we're doing now is make it so every service when you cretae it you get the standard dashboard with the same set of things that we all agree are useful things to know about from your service and you get this without having to do any work at all."
Uber's growing pains were in part caused by a lack of standardization across their system of microservices which made important things like tracing, profiling, code sharing, and error recovery almost impossible. While it's theoretically nice to build everything in isolation, it's important that teams are building upon some degree of common infrastructure so as to avoid reinventing the wheel and to make the operations team members lives easier.
A lot of companies take on the added complexity of microservices long before they're in a position where they encounter the issues microservices are supposed to solve. While planning ahead and being ready for the future is important, it's also important to ship early and often, and to release robust software which users love.
The high initial cost of adopting microservices isn't something you want to pay early on in a company's life where product-market fit has yet to be found or when your engineering team is too small to form a proper platform team. Below a certain levle of scale, a Majestic Monolith written in a modular and extensible fashion–perhaps a hexagonal architecture–makes far more sense than jumping straight into microservices and makes splitting things out easy(ish) in future when you absolutely need to.
The key insight we should hold on to when looking at microservice architectures is that microservices are an evolutionary response to real problems encountered by large organizations. The decision to start splitting out disparate services is not something which occurs in a vacuum–there is surrounding context which helps guide that intention.
It is fundamentally no different from any other engineering decision.
In the past few years, there has been a growing movement to adopt some of the principles of microservices inside the web browser. Microfrontends are pitched as a way to manage the sprawling complexity of frontend monoliths: by splitting up your frontend codebase into a collection of separate, smaller frontends you will–supposedly–unlock a greater level of developer productivity in much the same way microservices will.
More specifically, the goal of a microfrontend architecture is to reduce coupling across a web app and allow teams to release their own features independently from the rest of the organization. By breaking up a monolithic web app into smaller fragments, we hopefully wind up with a simpler system to reason about and win greater flexbility when it comes time to upgrade individual components of our app.
Something is clearly off about this comparison. The web browser is a very different environment from the data center, and these two deployment targets share few concerns. When we talk about microservices, we're talking about breaking out bits of functionalty into completely different binaries and then orchestrating this collection of services together.
A 1:1 translation of this to browserland means building your microfrontends separately and deploying them into their own iframes. One team is responsible for developing the "skeleton" app with a bunch of iframes which other teams are responsible for filling in. This is the approach which Spotify used to use, and you can still follow it if you want by using frameworks like
zoid–but this microfrontend strategy is rarely used because it's utterly impractical and completely destroys the user experience.
What are some of the issues you'll encounter if you go down the path of iframing everything?
- Your bundle size will explode. It's impossible to share common dependencies (like React) when everything is built in total isolation, which means you need to ship multiple copies of these libraries to the client. Some web apps live and die by the time it takes to get something on the user's screen.
- Modals are a nightmare. Because iframes have such a strong isolation model, it's impossible to render something on top of the rest of the UI from within an iframe. If you want to show a real modal, you'll need to write a bunch of code in your skeleton app which your microfrontends interface with to show modals. And then after you've written that interface, you'll also need to come up with a way of returning data from that modal back to the microfrontend which originally rendered it. This extra code is coupling–which we were supposed to be trying to avoid.
- Other UX niceties are also hard: when a form fails to validate it's common to scroll the user's viewport up to the offending input, but this is really hard to do from within an iframe.
- Accessibility is downright impossible. One research paper recommended "if the application has accessibility requirements it is [best] to avoid using iFrames", but the problem with that recommendation is that every single application has "accessibility requirements."
Even if you do manage to work around these issues (or just decide to ignore them), you'll find that the benefits you win from this architecture aren't quite as good as you might have hoped. Microservices enable you to use a variety of languages on the backend–perhaps Python for data engineering and analytics, Go for batch jobs, and Node.js for your API–but on the browser you can only write code in one language anyway1.
Resource constraints are also completely different–it would be either impossible or incredibly cost-prohibitive to run all of Facebook's backend code on one machine because there's so much code that you'd need an incredibly well-specced server. Vertical scaling of backend systems just doesn't work beyond a certain point, and breaking things out so you can scale horizontally turns out to be far more economical once you reach that tipping point. In the browser you're always constrained by the limitations of consumer hardware. Your frontend codebase can't grow infinitely anyway because if it did then eventually nobody would be able to run it. The level of scale is far lower.
And finally, it's just really hard to write a truly decoupled frontend API. How do you manage something like a light mode / dark mode switch without some sort of shared context across your microfrontends? How do you share caches across microfrontends to avoid performance issues from overfetching? Of course, you need to add more code to your skeleton app and couple your microfrontends to that interface. The amount of shared context on the backend is far lower than in the browser, which makes the data center far more amenable to service-oriented architectures.
These drawbacks are why Spotify moved off of their inhouse iframe-based microfrontend framework in favor of a more traditional SPA, and why Klarna abandoned iframes for their checkout app. The benefits are usually outweighed by cost.
Iframe-based microfrontends died before they ever really took hold, and today microfrontend frameworks such as
single-spa instead take an "integrated" approach to microfrontends wherein the microfrontends are all rendered to the same browser context.
This sidesteps the limitations of iframes, but at this point it's very unclear what the advantages of a microfrontend even is. What does the term "microfrontend" mean if everything's running on the same shared browser context?
The documentation site for
single-spa has the following to say:
"Think of the DOM as the shared resource that your microfrontends are owning. One microfrontend's DOM should not be touched by another microfrontend …"
ReactDOM.findDOMNode (long deprecated) or the native DOM APIs like
document.querySelector and then mutating that node(s).
single-spa doesn't use iframes to enforce isolation between microfrontends, there is absolutely nothing stopping you from using the DOM APIs to modify another microfrontend's DOM in the exact same way you can with any other UI framework. You aren't getting any additional safety or encapsulation compared to how you usually make web apps.
And looking at code samples, well, the examples on their website look identical to "normal" web apps. Your microfrontends essentially get bundled up as packages, and you either import directly from those packages if you want to use something programmatically (really) or use their routing system to assign microfrontends to routes.
How is this any different from just writing normal Node packages and importing those packages from your skeleton app? I'm not sure, and I don't think there actually is one. Their boilerplate
singleSpaReact function for wrapping React components with
single-spa lifecycles is admittedly somewhat nice because it forces you write an error boundary and because
single-spa relies on browser-resolved imports you get code splitting for "free", but are those really perks?
Is it really that hard to use
React.lazy or render some error boundaries manually? If a particular microfrontend is of sufficient complexity you'll probably want to pepper some error boundaries throughout it anyway, so does not having to render one at the root really matter all that much?
What you really want when building large and complicated web apps is a strong component model and an engineering culture that champions ownership, accountability, and autonomy. There's no fundamental reason why a monolithic web application should require the sign off of every single person working in the repo before something can be shipped because, again, there's nothing stopping you from using something like Lerna to break things out into separate packages and granting jurisdiction over those packages to your various teams. This isn't a technical problem: it's an organizational one.
Microfrontends and self-driving cars
When I think about microfrontends, I think about the "Self Driving Cars? Scam!" talk by George Hotz. Hotz is the founder and CEO of Comma.ai–the Android of self-driving cars, if Tesla is the iPhone. They manufacture and produce a cheap kit you can retrofit to a wide range of automobiles to enhance their out of the box lanekeeping and cruise control.
Hotz is critical of the concept of "fully self-driving cars" and the cottage industry of consultants, ethicists, and entrepreneurs which have sprung up around the concept. Zoox raised US$800m about four years ago, and their only accomplishment to date is being acquired by Amazon for about a third of their original valuation. Zoox is far from the only company in this space with that kind of track record.
And with respect to the ethics of autonomous vehicles?
"‘Oh well if I’m driving down the street and there’s a baby in the way but there’s a tree do I swerve and kill the human…’ has this ever happened to a human? Has this ever been a real scenario? […] This captures a conversation about self-driving cars that’s completely disproportional to any remote impact it may have but a lot of people are making money off of this."
There is a discussion to be had around the real-world implications of having AI behind the wheel, but I'm inclined to agree with Hotz' point that a lot of the discussion taking place isn't particularly profound. Self-driving cars have been a hit in pop culture for many decades and have captured the imaginations of us all.
Now that we find ourselves seemingly on the cusp of realizing this dream, there are so many people falling over themselves in a vain attempt to be recognized as thought leaders in the space. Ethics in autonomous vehicles is a veritable gold mine with shockingly low barrier to entry.
Hype in software
I think software's in a similar state. The
single-spa package's documentation consists of a brief YouTube playlist and links to a website where you can purchase online courses and in-person training sessions from the creator of
single-spa. Undoubtedly there are a ton of companies out there in desparate need of rethinking their approach to frontend development–I've seen a lot of gnarly codebases in my time. But did we need to rebrand component-driven design as
DevOps is the same. The whole point of DevOps is that if you're actually any good at it, the "Ops" part progressively fades into the background which frees you up to spend more time on "Dev". But what we actually see in the real world is nothing of the sort; there's a big marketplace of products, services, and consultants out there who are all too happy to encourage companies to pursue new shiny things and continuously churn their IaC configuration. The people who really, truly care about obsoleting the "Ops" in "DevOps" are the ones working on developer tooling and the actual cloud platforms.
Unlike microservices, no large tech company is evangelizing microfrontends because the phrase doesn't mean anything. It's an unnecessary reskin of a concept everyone is already familiar with. One of the supposed advantages of microservices is that you can avoid politics and do things your own way within the bounds of the services your team owns.
This isn't actually an advantage because what it really means is you keep all of your biases and lose out on the benefits of cross-team standardization, but the idea is really appealing to engineers. When was the last time you found yourself thinking that swapping from library \$X over to library \$Y would solve all of your problems? Probably earlier this very week. We have a tendency of trying to look for a silver bullet which will rid our codebases of technical debt and automate the boring glue bits of our job which is the very thing the term "microfrontend" is trying to take advantage of.
There is so much material online about component-driven development that it's hard to build an audience talking about the topic. But if you put lipstick on the pig and dress it up slightly differently, you can generate a lot of hype. Hype driven development and the closely related Résumé-driven development are bad for our industry because they prioritize picking shiny new toys instead of the correct tools for the job.
When it comes down to it, all engineering disciplines are incredibly practical endeavors. As professionals we need to be pragmatic when designing systems and recognize that there are very few free lunches. I don't want civil engineers to start building bridges with some fancy new material they heard of yesterday from some random blog post on the Internet. While software engineers rarely kill people when they make a bad call, we do have a poor track record of getting things delivered on time and under budget.
Let's call a spade a spade, and focus on getting things done. The predatory arm of the consulting industry can't sow chaos by rebranding common terminology if we're disciplined and conscientious about our work.