The Problem With Building Everything as a Micro Frontend
Micro frontends can give teams real independence, but that independence comes with a cost. A project involving multiple partners taught me that splitting ownership across frontend applications can move complexity rather than remove it.
A while ago, I worked on a project that involved several organizations contributing different parts of a larger platform.
The end product was a dashboard. From the user's point of view, it was one application with one purpose.
Behind the scenes, things were quite different.
Each partner was responsible for a different piece of functionality. One might own analytics, another might provide search or data processing, while another could be responsible for a completely different domain.
That part wasn't unusual. Large projects often work this way.
What made this project interesting was one particular requirement.
Partners weren't expected to expose only their functionality through APIs. Each partner had to deliver a complete working frontend for their part of the system.
Our job was then to integrate all of those frontends into one dashboard.
At first, the idea sounded reasonable.
Every partner could build their part independently. Nobody had to wait for the central frontend team to implement their UI. Partners could test their applications themselves, demonstrate them separately, release updates when needed, and keep control over the full stack they were responsible for.
Ownership was clear.
Partner A owned its service and its frontend.
Partner B owned another service and another frontend.
We owned the dashboard that had to bring everything together.
On an architecture diagram, it looked clean.
Partner A UI ─┐
Partner B UI ─┼──> Dashboard
Partner C UI ─┤
Partner D UI ─┘
In reality, that last arrow contained most of the complexity.
The Requirement That Looked Reasonable
There were good reasons for structuring the project this way.
When several companies or research partners work together, nobody wants every small frontend change to pass through one organization. If Partner A knows its domain best, letting that partner build the UI for it seems logical.
It also makes the deliverable easy to define.
Instead of saying:
Provide an API that another team will eventually turn into a user interface.
you can say:
Provide a working application that demonstrates your functionality.
That's much easier to validate during development.
Each partner can run their application locally. They can show it during meetings. They can test their backend and frontend together. They don't need detailed knowledge of the dashboard's internal codebase.
The problems appear later.
Eventually, these applications aren't being shown separately in project meetings anymore.
They're being placed in front of actual users.
And users don't know that four companies built four different parts of the screen.
They see one dashboard.
They expect one product.
When Every Feature Becomes Its Own Application
A simpler version of the architecture could have looked something like this:
Partner A API ─┐
Partner B API ─┤
Partner C API ─┼──> Dashboard UI
Partner D API ─┘
Each partner would still own its domain.
They would expose the functionality and data required by the dashboard, ideally through well-defined APIs. A single frontend would then decide how those capabilities should appear to the user.
Instead, we had something closer to this:
Partner A
├── API
└── Frontend
Partner B
├── API
└── Frontend
Partner C
├── API
└── Frontend
Partner D
├── API
└── Frontend
↓
Dashboard Shell
The architectural difference is easier to see side by side:
That small architectural difference changes quite a lot.
Every frontend now has its own build process.
Its own dependencies, assumptions, error handling, and API client.
Possibly its own state management.
Maybe even its own framework.
And because those applications came from different teams and organizations, you can't assume that everybody made the same technical choices.
This is where the idea of micro frontends becomes attractive.
Instead of trying to merge several independently developed applications into one large repository, you keep them independent and compose them at runtime.
In theory, everyone gets to keep their autonomy.
In practice, you start discovering that frontend applications share far more responsibilities than the architecture diagram suggests.
The Complexity Didn't Disappear. It Moved.
Micro frontends can absolutely reduce one type of complexity.
Partner A doesn't need to understand Partner B's code.
Repositories can stay smaller.
Teams don't constantly modify the same application.
A release by one team doesn't necessarily require rebuilding every other frontend.
Those are real benefits.
But the product hasn't suddenly become simpler.
The complexity has moved somewhere else.
With a traditional frontend, a large amount of complexity exists inside the application.
Application
├── Authentication
├── Routing
├── State
├── Components
├── API clients
├── Permissions
├── Analytics
├── Error handling
└── Styling
When you split that application into micro frontends, many of those problems still exist.
You just have to solve them between applications.
Authentication becomes an integration problem, routing an ownership problem, and shared state a communication protocol.
Dependencies become runtime compatibility questions, styling a coordination problem, and deployment a version compatibility problem.
Nothing disappeared.
It changed shape.
Independence Works Until Things Need to Talk
The easiest micro frontend is one that knows nothing about any other micro frontend.
Imagine a dashboard where each route belongs entirely to one application.
/dashboard/search → Search application
/dashboard/analytics → Analytics application
/dashboard/reports → Reporting application
/dashboard/admin → Administration application
That's fairly clean.
The shell handles global navigation and authentication. Once the user enters /dashboard/search, the search team owns almost everything underneath that route.
There isn't much communication between domains.
Real dashboards aren't always that tidy.
Suppose the dashboard contains a global organization selector.
The user changes from:
Organisation A
to:
Organisation B
Now three different micro frontends need to reload their data.
Who owns that state?
The shell?
A shared library?
Browser storage?
An event bus?
URL parameters?
Now suppose there is also a global date filter.
And user permissions.
And language.
And feature flags.
Pretty soon the supposedly independent applications need quite a bit of shared context.
This doesn't mean micro frontends failed.
It means the boundaries weren't as independent as they first appeared.
Shared State Becomes an API of Its Own
Inside one React application, passing shared information around is usually straightforward.
You might have something similar to:
<App>
<AuthProvider>
<OrganisationProvider>
<DateRangeProvider>
<Router />
</DateRangeProvider>
</OrganisationProvider>
</AuthProvider>
</App>
Whether that's the best way to manage state is another discussion, but all parts of the application live inside the same runtime.
With micro frontends, you can't make that assumption.
You might have:
Dashboard Shell
├── Analytics MFE
├── Reports MFE
└── Search MFE
Now imagine all three need the current user.
You have to establish a contract.
Perhaps the shell passes user information when mounting each application.
Maybe everyone reads the same authentication cookie.
Perhaps authentication is handled by a shared SDK.
The same decision has to be made for permissions, selected organizations, language, global filters and anything else that crosses application boundaries.
You eventually end up with another API.
It may not use HTTP, but it's still an API.
interface DashboardContext {
userId: string;
organisationId: string;
locale: string;
permissions: string[];
}
Once several independently released applications depend on that interface, changing it requires the same care you'd give to any public API.
A field can't casually disappear because someone refactored the shell.
Backward compatibility starts to matter.
Versioning starts to matter.
Documentation definitely matters.
That's a very different situation from changing an internal React context inside one repository.
The Dashboard Still Has to Look Like One Product
Technical integration was only part of the problem.
The harder part was often making everything feel like it belonged together.
Users don't care about project structure.
They don't care which organization received funding for which work package.
They certainly don't care that one screen uses React while another partner chose Vue.
If all of it appears under the same product name, they expect consistency.
Buttons need to behave similarly, and typography needs to match. Forms shouldn't suddenly change style between pages, and spacing shouldn't be completely different.
Loading states need some consistency. Error messages should make sense. Accessibility can't stop at the micro frontend boundary.
Even small things become noticeable.
One partner might use a 14px base font.
Another uses 16px.
One uses rounded inputs.
Another doesn't.
One opens dialogs with one keyboard behaviour, another does something slightly different.
Each application looks fine by itself.
Put them next to each other and the dashboard can start looking like several applications glued together.
The obvious answer is a shared design system.
That helps a lot.
But a shared design system also introduces another dependency between supposedly independent applications.
Now you might have:
Dashboard → Design System 4.2
Analytics MFE → Design System 4.2
Reports MFE → Design System 3.8
Search MFE → Design System 4.0
Someone has to maintain those versions.
Someone has to tell partners when breaking changes happen.
Someone needs to make sure an old micro frontend doesn't continue shipping a two-year-old version because nobody has touched that module recently.
Again, the complexity hasn't disappeared.
We've moved it into coordination.
CSS Gets Surprisingly Political
CSS deserves a special mention because it's one of those problems that looks trivial until several independent applications share the same browser window.
Who owns the global styles?
Can a micro frontend reset button styles?
What about body?
Who sets the base font?
Can one application add:
* {
box-sizing: border-box;
}
without knowing what the other applications expect?
What happens when two teams use the same generic class name?
.container {}
.header {}
.active {}
CSS Modules, scoped styles, CSS-in-JS and Web Components can reduce these risks.
They don't remove the need for rules.
The same applies to z-index.
A modal inside one micro frontend might use:
z-index: 9999;
because that was enough when the application ran by itself.
Then another frontend does the same.
Then the dashboard header needs to remain above both.
Before long, someone has created a spreadsheet explaining which application owns which z-index range.
I'm exaggerating slightly.
Only slightly.
These are small problems individually. A few CSS conflicts won't destroy an architecture.
But micro frontend systems tend to accumulate this kind of friction.
Versioning Moves Into the Browser
Dependency management also changes.
Imagine everyone started with the same version of React.
Shell → React 19
Analytics → React 19
Reports → React 19
Search → React 19
Six months later:
Shell → React 19
Analytics → React 19
Reports → React 18
Search → React 19
Maybe the reporting application hasn't been updated yet.
Do we ship React twice?
Do we force all partners to upgrade?
Do we declare React as a shared singleton?
What happens if a new module needs a library version that isn't compatible with another application?
Tools such as Module Federation can help manage shared dependencies and runtime composition.
But this is where I think discussions around micro frontends sometimes focus on the wrong thing.
The hard question isn't:
Can Module Federation share React between these applications?
It probably can.
The more important question is:
Do we want independently owned applications to depend on the same runtime version?
That's an architectural decision.
Module Federation is only one technical way of implementing the answer.
Independent Deployment Isn't Free
One of the strongest reasons to use micro frontends is independent deployment.
And this part really is attractive.
The analytics team fixes a bug.
They deploy Analytics v32.
Nobody rebuilds Search.
Nobody asks the Administration team to release anything.
Great.
But production is no longer simply:
Dashboard v42
It's more like:
Shell v12
Analytics v32
Reports v18
Search v27
Administration v9
Tomorrow:
Shell v12
Analytics v32
Reports v19
Search v27
Administration v9
Two days later:
Shell v13
Analytics v32
Reports v19
Search v28
Administration v9
You aren't only testing releases anymore.
You're testing compatibility between releases.
If the contracts are strong and the teams respect them, this can work very well.
But those contracts don't maintain themselves.
You need automated tests around integration points.
You need some way to know which versions are deployed.
You need rollback plans.
You probably want staging environments that resemble production closely enough to catch integration problems.
And if multiple organizations own those applications, you also need communication.
Independent deployment reduces one kind of coordination while demanding maturity in another.
Debugging Gets More Interesting Too
Eventually someone reports:
The dashboard isn't loading.
That's not quite enough information anymore.
Did the shell fail? Authentication? A remote bundle returning a 404?
Maybe a partner deployed a broken JavaScript bundle, or the API behind that micro frontend failed. Maybe the shell passed an unexpected context value, two deployed versions didn't match, or one application threw an uncaught exception while everything else continued working.
With one frontend, your monitoring setup usually has a fairly clear idea of what application the user was running.
With several independently deployed frontends, observability becomes more important.
At minimum, I want an error report to tell me something like:
Shell: 13.2.0
Analytics: 32.1.4
Reports: 19.0.2
Search: 28.3.1
User route: /dashboard/reports
Otherwise you're debugging a moving target.
Logs also need enough context to tell you which application produced the error.
The browser might contain code owned by four different teams. Seeing TypeError: Cannot read properties of undefined isn't especially helpful if you can't quickly tell who owns the failing code.
Distributed frontend architecture needs distributed observability.
That sentence would've sounded strange to me years ago.
It doesn't anymore.
Could We Have Just Used APIs?
At some point during the project, I kept coming back to a much simpler question.
Did every partner actually need to provide a frontend?
What if they had delivered APIs instead?
The architecture could have been:
Partner A ── API ──┐
Partner B ── API ──┤
Partner C ── API ──┼──> Dashboard
Partner D ── API ──┘
The partners would still own their functionality, design their backend services, and expose their domain through documented contracts.
But the dashboard team would own the presentation layer.
That would mean one routing system, one authentication integration, one design system version, and one application state model.
One accessibility approach. One error monitoring setup. One place to handle global filters and user context.
Of course, there would be a cost on that side too.
The dashboard team would become responsible for more implementation work.
Partners couldn't change their interfaces independently.
UI work could become a bottleneck if the central team wasn't large enough.
Changes to an API might require coordinated frontend work.
So I wouldn't claim that API-first was automatically the right answer for our project.
Project structures, contracts and partner responsibilities often exist for reasons that go beyond software architecture.
Still, I think it's a question that should have been asked very early:
Does this capability need an independently owned user interface, or does it just need to expose functionality to the product?
Those are very different requirements.
A Large Frontend Isn't Automatically a Micro Frontend Problem
This experience also changed how I think about another common argument.
A frontend gets large.
There are hundreds of components.
Several developers work on it.
Pull requests touch unrelated areas.
Someone eventually says:
We should split this into micro frontends.
Maybe.
But size alone isn't enough.
Sometimes the actual problem is simply that the application has poor internal boundaries.
Imagine this:
src/
├── components/
├── hooks/
├── utils/
├── services/
└── pages/
Everything imports everything.
Business rules live inside random components.
API calls happen wherever they're needed.
Shared state grows until nobody knows who owns it.
At that point, the frontend is painful to maintain.
But distributing it across several deployments doesn't automatically fix those boundaries.
You can quite easily end up with several badly designed applications instead of one.
A modular frontend might be enough.
src/
├── app/
│
├── domains/
│ ├── analytics/
│ │ ├── api/
│ │ ├── components/
│ │ ├── hooks/
│ │ └── routes/
│ │
│ ├── reporting/
│ ├── search/
│ └── administration/
│
├── shared/
│ ├── components/
│ ├── auth/
│ └── utils/
│
└── design-system/
There are still boundaries.
Analytics doesn't reach directly into Search.
Shared code has clear ownership.
Domains can have their own API layers and tests.
The application remains one deployment unit.
You get many of the code organization benefits people expect from micro frontends without introducing runtime distribution.
I've started thinking about this as an important middle ground.
The choice isn't:
Huge messy frontend
OR
Micro frontends
There's quite a lot in between.
Each step gives teams more autonomy, but it also introduces more coordination and runtime complexity.
When Micro Frontends Do Make Sense
After all that, I'd still use micro frontends.
Just not because an application has become large.
The strongest case appears when the organizational boundaries already exist.
Imagine a company with several autonomous teams:
Search Team
Checkout Team
Account Team
Administration Team
Analytics Team
Each team owns its business domain.
They have their own roadmap, backend services, developers, and release schedule.
They deploy independently.
They operate what they build.
Now asking all of those teams to contribute to one giant frontend repository can become the artificial constraint.
The organization is already distributed.
The architecture isn't.
Micro frontends can fix that mismatch.
The same argument applied to our project. Different partners really were different organizations. Giving them independent delivery boundaries wasn't an invented problem.
The difficulty came from how much integration the final product required.
The more independent the domains are, the better the model works.
The more often they need to share state, visual components and workflows, the more expensive those boundaries become.
That's the part I'd look at before choosing the architecture.
Not Every Component Needs to Become a Micro Frontend
The word "micro" can also send teams in a strange direction.
If some separation is good, smaller separation must be better.
That can lead to something like this:
Dashboard
├── Header MFE
├── Navigation MFE
├── Filters MFE
├── Charts MFE
├── Notifications MFE
├── Search MFE
└── Profile MFE
Now almost every interaction crosses application boundaries.
A filter changes and the chart application needs to know.
The navigation changes and the profile application needs to react.
The notification application needs authentication.
Everything needs the design system.
Everything needs global configuration.
You've created independence at the code level while creating heavy coupling at runtime.
I'd much rather have coarse domain boundaries.
/dashboard/search → Search MFE
/dashboard/reports → Reports MFE
/dashboard/admin → Administration MFE
Each application owns a meaningful slice of the product.
This also makes failure easier to contain.
If Search fails, Reports can still work.
If the Reports application is being deployed, Administration doesn't care.
The boundary reflects something real.
That's the key.
The goal shouldn't be to create the smallest possible frontend.
It should be to find the places where independence already makes sense.
Start With the Boundary, Not the Tool
Micro frontend discussions often jump quickly to implementation.
Module Federation?
Web Components?
iframes?
npm packages?
Server-side composition?
Those decisions matter, but I'd make them later.
First I'd draw the boundaries.
Who owns Search?
Who owns Reports?
Does Search ever need internal state from Reports?
Can they release independently?
Can one be unavailable while the other still works?
Do they share the same users?
Do they need the same global filters?
Who owns navigation?
Who owns authentication?
Who owns the design system?
If those answers are messy on a whiteboard, Module Federation won't make them cleaner.
It might make the applications load.
That's not the same thing.
Architecture should describe ownership before it describes bundling.
The Real Trade Is Simplicity for Autonomy
Looking back at that dashboard project, I don't think the useful lesson is that micro frontends were a mistake.
The architecture solved a real problem.
Several partners could build their parts independently.
They didn't need access to one shared frontend repository.
They could develop using their own processes.
They could test their functionality end to end.
They had clear ownership over what they delivered.
Those benefits mattered.
But we paid for them at the integration layer.
The dashboard had to make multiple applications look like one product. Shared state needed contracts, authentication had to cross boundaries, and styling required coordination. Dependency versions mattered, deployments created new combinations of running software, and debugging required knowing which application failed.
None of those problems makes micro frontends bad.
They're the price of the autonomy we asked for.
And that's probably the part I'd consider first if I were designing the same kind of system again.
Before splitting a frontend into independently deployed applications, I'd ask what problem we're actually trying to solve.
If several autonomous teams or organizations genuinely need to own and release separate parts of the product, micro frontends can be a very good fit.
If one team owns the whole application and the main complaint is that the codebase has become difficult to maintain, I'd start somewhere else.
Fix the boundaries.
Separate the domains.
Make ownership clear.
Build a modular application.
You can always distribute it later if the organization eventually needs that freedom.
Going the other way is usually harder.
A large frontend isn't automatically a micro frontend problem. Sometimes it's just a frontend that needs better architecture.