Micro-frontends have become one of the most debated architectural patterns in frontend development. Proponents say they enable independent team deployment and scaling. Critics say they add unnecessary complexity. Both are right — the question is which trade-off matters more for your specific situation.
I have a unique perspective on this because I was the principal architect of a micro-frontend architecture for AXA Switzerland's B2C strategy, and I've also chosen NOT to use micro-frontends in subsequent roles. Both decisions were correct in context.
The Problem Micro-Frontends Actually Solve
Micro-frontends solve one problem well: organizational independence. When you have multiple teams that need to ship features to the same user-facing application on different schedules, and coordination between those teams has become a bottleneck, micro-frontends can help.
That's it. They don't make your app faster (usually the opposite). They don't make your code cleaner. They don't reduce complexity. They trade technical complexity for organizational flexibility.
At AXA, we had exactly this problem. Multiple teams owned different parts of the customer-facing B2C experience: insurance quotes, claims, policy management, and onboarding. Each team had its own sprint cadence, its own priorities, and its own release schedule. A monolithic frontend meant that a bug in the claims module could block a release of the quotes module. Teams were spending more time coordinating deployments than building features.
How We Did It at AXA
Our approach was pragmatic, not ideological. We didn't chase the microservices dream of "every team picks their own framework." We standardized on React and TypeScript — the freedom was in deployment independence, not technology choice.
Each module was a standalone application that could be built, tested, and deployed independently. A thin shell application handled routing, authentication, and shared layout. Modules communicated via a defined event bus, not direct imports. The key difference to runtime approaches: aggregation happened at build time, but each app could still deploy individually. This meant the quotes team could deploy three times a day while the claims team deployed weekly, without any coordination.
// Simplified module registration in the shell app
interface MicroFrontendModule {
name: string;
basePath: string;
load: () => Promise<{ mount: (el: HTMLElement) => void }>;
}
const modules: MicroFrontendModule[] = [
{
name: 'quotes',
basePath: '/insurance/quotes',
load: () => import('quotes-module/bootstrap'),
},
{
name: 'claims',
basePath: '/claims',
load: () => import('claims-module/bootstrap'),
},
];
// Each module mounts into its container independently
// Shell handles routing and passes context via eventsThe Real Costs Nobody Talks About
Here's what the conference talks leave out:
Shared state is hard. Really hard. When two micro-frontends need to share user context, cart state, or notification counts, you're essentially building a distributed system in the browser. We spent weeks getting authentication state to sync reliably across modules without race conditions.
Consistent UX is expensive. When modules are deployed independently, visual inconsistencies creep in. Module A ships with updated button styles while Module B still has the old ones. We solved this with our Web Components-based style guide library — framework-agnostic components that every module consumed. But building and maintaining that library was a significant investment.
Performance overhead is real. Each module carries its own runtime overhead. Even with shared dependencies extracted into a common chunk, the initial load is heavier than a single application. At AXA, our Time to Interactive increased by roughly 800ms compared to the monolith. We optimized this with aggressive lazy loading and prefetching, but the overhead never fully disappeared.
Developer experience suffers. Running the full application locally means orchestrating multiple dev servers. Debugging across module boundaries is painful. Integration testing requires all modules to be available. We built significant tooling to make this workable, which was itself a maintenance burden.
When I Chose Not to Use Them
At Migros, building Bikeworld and Micasa, we had a similar surface-level problem: multiple stores sharing some infrastructure. The instinct might have been to use micro-frontends. But I chose a PNPM monorepo with Turbo instead.
Why? Because the organizational problem was different. We had one team building both stores, not multiple teams needing independent deployment. The stores shared a design system and some infrastructure but had different product pages and checkout flows. A monorepo with shared packages gave us code reuse without the deployment complexity.
At Vontobel, building a single complex financial platform, micro-frontends would have been pure overhead. One team, one deployment target, one release cycle. A well-structured Next.js application with clear module boundaries — enforced by linting rules and code review, not runtime isolation — was the right answer.
At UBS, we actually use micro-frontends with Webpack Module Federation across 3 teams. We have a Turborepo-based monorepo, but modules are aggregated at runtime — each team can deploy independently. It's a different approach from AXA, where aggregation happened at build time. Both work, but runtime federation gives us faster independent deployments while the monorepo keeps shared code and tooling consistent.
The Decision Framework
After living with micro-frontends and choosing alternatives, here's my framework for when they make sense:
Use micro-frontends when: you have 4+ teams shipping to the same application, deployment coordination has become a measurable bottleneck (not just an annoyance), teams have genuinely different release cadences, and you have the engineering capacity to build and maintain the required infrastructure (shell app, shared libraries, tooling, CI/CD pipelines).
Don't use micro-frontends when: you have fewer than 3 teams, you want to use different frameworks per module (the UX cost is too high), your application has heavy cross-module data dependencies, you don't have dedicated platform engineering capacity, or you're choosing them because they're trendy rather than because you've felt the pain they solve.
// Alternatives that solve similar problems with less cost
// 1. Monorepo with package boundaries
// Good for: shared code, independent builds, one team
// pnpm-workspace.yaml + turborepo
// 2. Module-level code splitting in a single app
// Good for: lazy loading, team ownership areas
const AdminModule = lazy(() => import('./modules/admin'));
const QuotesModule = lazy(() => import('./modules/quotes'));
// 3. Feature flags for independent feature releases
// Good for: decoupling deployment from release
if (flags.newCheckout) {
return <NewCheckout />;
}Architecture Is About Trade-Offs
The best architecture is the simplest one that solves your actual problems. Not the problems you might have in two years. Not the problems the company that wrote that Medium article had. Your problems, today.
Micro-frontends solved a real problem at AXA with build-time aggregation. At UBS, runtime federation with Webpack Module Federation fits 3 teams working from a monorepo. A monorepo solved a real problem at Migros. A well-structured single application solved a real problem at Vontobel. The pattern didn't change — understand the problem first, then choose the simplest solution that addresses it.
If that sounds like YAGNI applied to architecture, it is. The principles are the same at every level of abstraction.

