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 micro-frontend was its own JS bundle deployed to the server. Vendor code was extracted into a shared chunk so apps didn't duplicate dependencies. We built custom Rollup tooling that let us release everything at once through AEM CMS, but also allowed individual teams to override just their JS bundle on the server for hotfixes — no full release needed.
An aggregator tied the apps together, and a docking class handled cross-app communication — shared state, user context, and tokens. Each app auto-created its own shell and could decide whether it required authentication or ran anonymously. This meant the quotes team could deploy independently while the claims team worked on their own schedule.
On top of this, I founded a common component library built with Web Components — framework-agnostic, so every team consumed the same UI building blocks regardless of their app's internal choices. It plugged directly into the micro-frontend architecture: consistent UX across all apps, maintained by a dedicated style guide team.
// Each micro-frontend = its own JS bundle on the server
// Vendor JS is shared across all apps in a common chunk
// Rollup builds each app independently
// apps/quotes/rollup.config.js → /server/static/quotes.js
// apps/claims/rollup.config.js → /server/static/claims.js
// shared/vendor.js → /server/static/vendor.js
// Aggregator collects all apps and provides a docking class
// for cross-app communication (shared state, auth tokens)
class AppDocking {
private shared = new Map<string, unknown>();
set(key: string, value: unknown) { this.shared.set(key, value); }
get<T>(key: string): T { return this.shared.get(key) as T; }
}
// Each app auto-creates its own shell and decides
// whether it needs an access token or runs anonymously
interface AppConfig {
name: string;
entry: string; // /server/static/quotes.js
container: string; // #quotes-root
requiresAuth: boolean; // app decides this
}
// Deploy all apps at once via AEM CMS release,
// OR override individual JS on the server for hotfixes
// App X deploys → only /server/static/x.js gets replacedThe 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 a large Swiss bank, I took micro-frontends in a different direction: Webpack Module Federation with a Turborepo-based monorepo across 3 teams. Unlike AXA's custom Rollup approach where JS bundles lived on the server and got overridden individually, Module Federation aggregates at runtime — the shell loads remote modules on demand. The monorepo gives us shared tooling and consistent code standards, while federation gives each team full deployment independence. Two very different implementations of the same principle: teams that can ship without waiting for each other.
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 — custom Rollup bundles on a server, docking class for shared state, individual deploy capability through AEM CMS. At a large Swiss bank, the same problem got a different solution: Webpack Module Federation with runtime aggregation from a monorepo. Both work. The implementation differs because the constraints differ. 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.

