Micro-frontends (MFEs) apply the same idea as microservices—independent delivery and ownership—but to the UI layer. Done well, they let multiple teams ship a single product without stepping on each other. Done poorly, they turn your frontend into a slow, inconsistent patchwork.
This post focuses on what matters in real app development: when MFEs make sense, the architectures you can choose, and the non-obvious tradeoffs that show up in production.
What micro-frontends are (and aren’t)
A micro-frontend is a separately built, tested, and deployed UI “slice” that composes into a larger application at runtime or build time.
They are not:
- “Multiple SPAs in an iframe.” That’s usually a workaround, not an architecture.
- A free pass to ignore shared design systems, performance budgets, or accessibility.
- Automatically better than a well-structured monolith with feature modules.
A useful mental model: a micro-frontend should map to a product capability owned by a team (e.g., “Checkout,” “Profile,” “Marketplace”), not a technical layer (“Buttons,” “Forms”).
When micro-frontends are the right call
Micro-frontends shine when organizational complexity is the bottleneck.
They’re a good fit if you have:
- Multiple teams shipping to the same UI weekly or daily.
- Independent release cycles (team A must deploy without coordinating with team B).
- Divergent tech stacks you can’t unify quickly (e.g., legacy Angular + new React).
- Clear domain boundaries where “vertical slices” are real.
They’re usually the wrong call if:
- Your app is still finding product-market fit; MFEs add overhead.
- You have a single team or low deployment pressure.
- Your UI is highly interdependent (lots of shared state and cross-cutting flows).
Opinionated rule: if your “frontend platform” maturity is low (no design system, weak CI/CD, no observability), MFEs will amplify that pain.
Core composition patterns
There are three mainstream approaches. Each trades autonomy against consistency and performance.
1) Route-based composition (the most common)
The shell app owns top-level routing and loads an MFE for a route prefix (e.g., /checkout/*).
Pros:
- Clean boundaries; MFEs can be true vertical slices.
- Easier performance isolation and caching.
Cons:
- Cross-route shared UI (nav, notifications, auth gates) must be standardized.
2) Component-based composition (more granular)
A page can embed multiple MFEs as components (e.g., “Recommendations” widget alongside “Cart summary”).
Pros:
- Enables highly modular product surfaces.
Cons:
- Harder to manage shared state and consistent UX.
- Higher risk of duplicate dependencies and layout shifts.
3) Build-time integration (federated builds)
MFEs are versioned packages pulled into a host at build time (npm packages, internal registries) or via a federation mechanism.
Pros:
- Best for performance and determinism.
- Simpler security posture than runtime remote loading.
Cons:
- Less deployment independence (host rebuild required to pick up changes).
Implementation choices: iframe, web components, or module federation?
You can ship MFEs in different ways:
- Iframes: Strong isolation, weak integration. Great for untrusted content or legacy migration. Generally poor for seamless UX (routing, theming, auth, accessibility).
- Web Components: Framework-agnostic component model. Promising, but you still need a cohesive design system and careful styling strategy.
- JavaScript module federation / runtime remote modules: Popular for independent deploys. Powerful, but demands disciplined versioning and dependency governance.
A pragmatic approach many teams take: start with route-based MFEs plus a thin runtime loader, and only move to component-level composition when you’ve proven the need.
The hard parts nobody advertises
Micro-frontends fail for predictable reasons. Plan for these upfront.
Dependency duplication and performance regressions
If each MFE bundles its own copy of React, your users pay multiple times.
Mitigations:
- Define shared dependencies and enforce singleton versions where appropriate.
- Track bundle budgets per MFE and for the composed page.
- Prefer modern delivery: HTTP/2+, long-lived caching, code splitting, and prefetching.
Design consistency and UX fragmentation
Users don’t care that teams are independent; they see one product.
Mitigations:
- A real design system (tokens, components, typography, spacing) with versioning.
- Centralized accessibility standards and automated checks.
- The shell owns global UX elements: navigation, toasts, modals, error boundaries.
State management across MFEs
Global shared state becomes a hidden coupling point.
Better strategies:
- Keep state local to an MFE whenever possible.
- Use URL state and server state as the contract (REST/GraphQL) rather than shared client stores.
- If you must share, standardize on an event bus with explicit events (e.g., AUTH_CHANGED, CART_UPDATED) and schemas.
Authentication, authorization, and security
When loading remote code at runtime, the supply-chain risk increases.
Mitigations:
- Sign artifacts, lock down registries, and use strict integrity checks.
- Treat MFEs as production software with SAST/DAST and dependency scanning.
- Centralize auth in the shell and pass scoped tokens/claims to MFEs.
Testing: integration becomes the new unit
Each MFE can be perfectly tested in isolation and still break the product.
Mitigations:
- Contract tests between shell and MFE (events, public APIs, routes).
- End-to-end tests on composed environments (staging that mirrors production composition).
- Canary releases per MFE when runtime loading is used.
A practical reference architecture
A solid baseline looks like this:
- Shell/Container app
- owns routing, global layout, auth, feature flags, telemetry
- loads MFEs via a registry (manifest) keyed by route
- Micro-frontends
- own domain UI, domain API calls, and local state
- publish versioned artifacts and a small public surface (mount/unmount + capabilities)
- Shared platform packages
- design tokens + component library
- observability SDK (logging, metrics, tracing)
- utilities with strict version policies
Keep the contract small: “Here’s how you mount, here are the events you emit/consume, here are the routes you own.” Everything else is internal.
Migration strategy from a monolith
Don’t rewrite. Extract.
- Identify a boundary with low coupling (e.g., Settings) and move it behind a route.
- Build the shell capabilities early: auth, navigation, telemetry, error handling.
- Introduce a shared design system before you extract too many MFEs.
- Establish governance: versioning rules, dependency policies, and performance budgets.
If the first extraction doesn’t reduce deployment coordination pain, pause. MFEs are an operational investment; you should see organizational ROI quickly.
Conclusion
Micro-frontends are a team-scaling strategy disguised as an architecture. They’re worth it when independent delivery and ownership are your limiting factors—and when you’re ready to invest in a strong frontend platform: design system, CI/CD, observability, dependency governance, and integration testing.
Start with route-based composition, keep contracts explicit, and enforce UX and performance standards centrally. The goal isn’t “many frontends.” The goal is one product that can evolve quickly without collapsing under its own coordination overhead.