Intro To Micro Frontends


Microservices are great. They allow us to shape our organization into small teams, each specializing in their own part of the business. By taking a big monolithic backend and splitting it to microservices, we can have one team only be responsible for the auth microservice and another team only be responsible for the payments microservice and so on.

The key part is that microservices are loosely coupled to one another. Each team can develop and deploy independently. only requiring to interact with one another when they want to change their agreed upon contract(e.g. a REST api or a pub/sub message).

But what about the frontend? apps today are becoming bigger and bigger, we can’t have just a single team handle all the frontend. we want to break up our frontend into modular pieces like we did on the backend. this is called micro-frontend.

The simplest way we can do this is by splitting our app into multiple components. each component lives in it’s own repo and published as a package. The host app then uses that packages and displays the component.

This may work fine at first, you get type safety, and it is easy to implement since you probably already have other internal packages. But you will quickly realize that the team developing the package cannot deploy it’s part independently. For every new version of their package, they have to ask the host app to install their new version and re-deploy the host app.

This is called build time micro-frontend. while it may work fine for modules inside your team, or modules that rarely change. But being dependent on other teams to let you deploy your own code will hurt productivity too much.

What About a Monorepo?

A monorepo is worth mentioning here because it solves some of the friction of the build-time approach. Instead of publishing packages to a registry and waiting for consumers to upgrade, all the code lives in one repository. Teams can make cross-cutting changes in a single PR and tooling like Nx or Turborepo can figure out which packages were affected and only rebuild those.

But a monorepo doesn’t give you independent deployment. Everything still gets deployed together, or you have to put significant effort into your CI pipeline to achieve selective deployments. And migrating to a monorepo is a big change — you’re reorganizing how every team works, where their code lives, and how their pipelines are structured. For large organizations that’s rarely a quick win.

So a monorepo is a real improvement over scattered packages, but it’s not the same thing as a micro-frontend. It solves the developer experience problem, not the deployment independence problem.

The solution is to use runtime micro frontend. This means that each module can be changed in runtime without any other part of the system needing to be aware of it. Exactly like we do with microservices.

The most common example of runtime micro frontend in web applications is iframes. iframes allow you to easily embed a different website into your own.

But iframes come with serious drawbacks. They are fully isolated, which sounds great at first, but it means sharing state, styles, or routing between the host and the embedded page becomes painful. They also cause layout and accessibility issues, and search engines struggle to index their content. Full isolation is too much of a good thing.

So what do we do instead?

The Shell and the Remotes

A better architecture splits the app into two roles: a shell and one or more remotes.

The shell is the host app. It owns the page — the URL, the global navigation, the auth state. It decides which remote to load and where on the page to put it.

The remotes are the independent micro-frontends. Each one is owned by a different team, lives in its own repo, and deploys on its own schedule. The shell doesn’t know anything about a remote’s internals. It only knows where to fetch it and where to mount it.

This mirrors the microservices model almost exactly. The shell is the API gateway. The remotes are the services. The contract between them is minimal by design.

How Remotes Are Loaded

At runtime the shell fetches each remote from its own deployment URL and mounts it into the page. The most widely used mechanism for this is Module Federation, though the concept isn’t tied to any specific tool.

The key point is that the remote’s code is not bundled into the shell at build time. It’s fetched live. This is what makes independent deployment possible — the checkout team can push a new version of their remote and every user gets it the next time they load the page, without the shell ever needing to redeploy.

One tricky consequence of this is shared dependencies. If the shell and a remote both use React, you don’t want two separate copies of React running on the same page. The loading mechanism handles version negotiation so both sides end up sharing a single instance. Getting this wrong causes subtle, hard-to-debug bugs, so it’s one of the first things to think through when designing the system.

Communication Between Micro-Frontends

The cleanest communication model is also the most familiar one: props.

If all teams are using the same component framework — React, Angular, Vue — then a remote can just expose a component. The shell renders it and passes down whatever data it needs. The remote doesn’t know or care where that data came from. The contract is just the component’s props, which is explicit, type-safe, and easy to reason about.

This does come with an important constraint though. Unlike microservices, which are technology-agnostic — one service can be Go, another Python, and they talk over HTTP — micro-frontends are not. If different teams use different frameworks you lose the ability to share components cleanly. A React team can’t consume an Angular component without a lot of glue code. You end up with wrapper layers, duplicated state, and a lot of friction.

In practice this means the org needs to agree on a single framework and stick to it. Teams get autonomy over their repo, their pipeline, and their internal architecture — but the framework is a shared standard, not a team decision.

For cases where the shell needs to notify remotes about something that happened elsewhere — a user logging out, a global error — browser events are still a fine tool. But they should be the exception. If a remote needs data from the shell to function, pass it through props.

When Should You Use Micro-Frontends?

Micro-frontends solve a people and process problem as much as a technical one. The real question isn’t “can we do this?” but “do we actually need this?”

They make sense when:

  • Multiple teams are blocked by a shared deployment pipeline
  • Different parts of the product have genuinely different release cadences
  • You’re incrementally migrating a legacy monolith and can’t do it all at once

They’re overkill when your team is small, your app doesn’t have clear domain boundaries, or independent deployment isn’t a real constraint. The added complexity in build tooling, routing, shared dependencies, and cross-app communication is a significant ongoing cost.

Summary

Micro-frontends extend the microservices idea to the frontend. Build-time approaches are the simplest but require everyone to coordinate on deployments. Runtime approaches, where a shell loads independent remotes on the fly, give you true autonomy at the cost of more moving parts.

Keep remotes focused on a single user journey. Keep communication through props and treat browser events as a last resort. And start with a monolith — only reach for this architecture when team size or deployment independence become real, felt bottlenecks.