Frontend Architecture
Part 2 of 11Micro-Frontend Architecture: Worth the Complexity or Not?
Micro-Frontend Architecture: Worth the Complexity or Not?
Introduction
Micro-frontends promise the same benefits for frontend that microservices delivered for backend: independent teams, autonomous deployments, technology flexibility. The pitch is compelling—especially if you've felt the pain of a monolithic frontend where a single broken component blocks the entire release.
But here's the uncomfortable truth that conference talks don't emphasize: micro-frontends add significant complexity. Build tooling, runtime integration, shared dependencies, cross-team coordination, consistent UX, performance optimization—all become harder, not easier.
The question isn't whether micro-frontends can work. They can. Companies like Spotify, IKEA, and Zalando use them successfully. The question is whether they're worth it for your situation.
This guide provides an honest assessment. We'll cover what micro-frontends actually are, the real problems they solve, the complexity they introduce, implementation approaches, and most importantly—when you should and shouldn't use them.
Spoiler: most teams shouldn't.
What Micro-Frontends Actually Are
The Core Concept
MICRO-FRONTEND ARCHITECTURE:
════════════════════════════════════════════════════════════════════
TRADITIONAL MONOLITH:
─────────────────────
┌─────────────────────────────────────────────────────────────────┐
│ SINGLE FRONTEND APP │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Search │ │ Product │ │ Cart │ │ Checkout │ │
│ │ Module │ │ Module │ │ Module │ │ Module │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ One repo. One build. One deploy. One team (or tightly coupled │
│ teams). Change anything → rebuild and redeploy everything. │
└─────────────────────────────────────────────────────────────────┘
MICRO-FRONTEND ARCHITECTURE:
────────────────────────────
┌─────────────────────────────────────────────────────────────────┐
│ CONTAINER / SHELL │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Navigation │ Shared UI │ Auth Context │ Routing │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────┼────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Search │ │ Product │ │ Cart │ │
│ │ MFE │ │ MFE │ │ MFE │ │
│ │ │ │ │ │ │ │
│ │ Team: A │ │ Team: B │ │ Team: C │ │
│ │ React 18 │ │ React 18 │ │ Vue 3 │ │
│ │ Deploy: ◉ │ │ Deploy: ◉ │ │ Deploy: ◉ │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │
│ Separate repos. Separate builds. Separate deploys. │
│ Teams work independently. Composed at runtime. │
└─────────────────────────────────────────────────────────────────┘
THE KEY PROPERTIES:
───────────────────
• Each micro-frontend is independently deployable
• Each can be developed by a separate team
• Each can (theoretically) use different technologies
• They're composed together at runtime (or build time)
• Users see one unified application
What Problems They Claim to Solve
THE MICRO-FRONTEND PROMISE:
════════════════════════════════════════════════════════════════════
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 1. TEAM AUTONOMY │
│ ───────────────────────────────────────────────────────────── │
│ "Teams can work independently without stepping on each other" │
│ │
│ Monolith reality: │
│ • Merge conflicts in shared code │
│ • Waiting for other teams to finish │
│ • Coordinated releases │
│ • Blocked by breaking changes │
│ │
│ MFE promise: │
│ • Each team owns their slice │
│ • Deploy when ready │
│ • No coordination needed │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 2. INDEPENDENT DEPLOYMENTS │
│ ───────────────────────────────────────────────────────────── │
│ "Ship your changes without waiting for others" │
│ │
│ Monolith reality: │
│ • One team's bug blocks everyone │
│ • Release trains and coordination │
│ • All-or-nothing deploys │
│ │
│ MFE promise: │
│ • Deploy cart without touching search │
│ • Rollback one feature independently │
│ • Faster time to production │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 3. TECHNOLOGY FLEXIBILITY │
│ ───────────────────────────────────────────────────────────── │
│ "Use the right tool for the job" │
│ │
│ Monolith reality: │
│ • Stuck on old React version │
│ • Can't experiment with new frameworks │
│ • Big-bang migrations │
│ │
│ MFE promise: │
│ • Incrementally migrate to new versions │
│ • Try Vue for one feature │
│ • Gradual modernization │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 4. SCALABLE ORGANIZATION │
│ ───────────────────────────────────────────────────────────── │
│ "Add teams without adding coordination overhead" │
│ │
│ Monolith reality: │
│ • More developers = more conflicts │
│ • Communication overhead scales O(n²) │
│ • Codebase becomes unmaintainable │
│ │
│ MFE promise: │
│ • Bounded contexts = bounded teams │
│ • Scale teams independently │
│ • Clear ownership │
│ │
└─────────────────────────────────────────────────────────────────┘
The Real Costs
The Complexity Tax
WHAT THEY DON'T TELL YOU AT CONFERENCES:
════════════════════════════════════════════════════════════════════
┌─────────────────────────────────────────────────────────────────┐
│ │
│ SIMPLE MONOLITH │ MICRO-FRONTENDS │
│ ─────────────── │ ──────────────── │
│ │ │
│ Build: npm run build │ Build: Each MFE separately │
│ │ + orchestration │
│ │ + version compat │
│ │ │
│ Test: npm test │ Test: Each MFE unit tests │
│ │ + integration tests │
│ │ + E2E across MFEs │
│ │ + contract tests │
│ │ │
│ Deploy: Upload bundle │ Deploy: Each MFE separately │
│ │ + CDN config │
│ │ + version manifest │
│ │ + rollback strategy │
│ │ │
│ State: Redux/Zustand │ State: Per-MFE state │
│ │ + cross-MFE state │
│ │ + sync mechanisms │
│ │ │
│ Routing: React Router │ Routing: Shell router │
│ │ + MFE routers │
│ │ + route ownership │
│ │ │
│ Styling: CSS/CSS-in-JS │ Styling: Per-MFE styles │
│ │ + style isolation │
│ │ + shared design sys │
│ │ │
│ Auth: One implementation │ Auth: Shared across MFEs │
│ │ + token passing │
│ │ + session sync │
│ │ │
│ Shared deps: Just use them │ Shared deps: Version mgmt │
│ │ + deduplication │
│ │ + conflicts │
│ │ │
│ Performance: Standard │ Performance: Multiple loads │
│ │ + coordination │
│ │ + optimization │
│ │ │
│ Developer XP: Single context │ Developer XP: Which MFE? │
│ │ + local dev │
│ │ + debugging │
│ │ │
│ Complexity: ~1x │ Complexity: ~5-10x │
│ │ │
└─────────────────────────────────────────────────────────────────┘
The Hidden Problems
PROBLEMS MICRO-FRONTENDS CREATE:
════════════════════════════════════════════════════════════════════
1. DEPENDENCY HELL
─────────────────────────────────────────────────────────────────
MFE-A uses React 18.2.0
MFE-B uses React 18.1.0
MFE-C uses React 17.0.2 ← Can't even share context
Options:
a) Ship multiple React versions (bundle bloat)
b) Force everyone to same version (coordination!)
c) Shared singleton React (complex setup)
Now multiply by: React DOM, React Router, state library,
UI component library, utility libraries...
2. BUNDLE SIZE EXPLOSION
─────────────────────────────────────────────────────────────────
Monolith bundle: 250KB (gzipped)
Micro-frontends:
├── Shell: 80KB (React, router, shared UI)
├── Search: 90KB (own deps + feature code)
├── Product: 120KB (own deps + feature code)
├── Cart: 85KB (own deps + feature code)
└── Total: 375KB (50% increase!)
Even with shared dependencies, there's overhead:
• Module federation runtime
• Duplicate code that can't be shared
• Multiple framework bootstraps
3. INTEGRATION TESTING NIGHTMARE
─────────────────────────────────────────────────────────────────
How do you test that Search → Product → Cart flow works?
• MFE-A's tests pass ✓
• MFE-B's tests pass ✓
• MFE-C's tests pass ✓
• Together? Who knows!
Need:
• Integration environment with all MFEs
• Contract tests between MFEs
• E2E tests that span boundaries
• Version compatibility matrix
4. INCONSISTENT USER EXPERIENCE
─────────────────────────────────────────────────────────────────
Team A: "We like 8px border radius"
Team B: "We prefer 4px border radius"
Team C: "What's a design system?"
Result: Frankenstein UI
Solutions (all add complexity):
• Shared component library
• Design token system
• Cross-team design reviews
• UI consistency audits
5. CROSS-CUTTING CONCERNS
─────────────────────────────────────────────────────────────────
Authentication: Every MFE needs user context
Analytics: Events from all MFEs need correlation
Error tracking: Errors need source attribution
Feature flags: Consistent flag evaluation
A/B testing: Experiments spanning MFEs
Accessibility: Consistent focus management
Each of these is trivial in a monolith.
Each becomes a project in micro-frontends.
6. COMMUNICATION OVERHEAD
─────────────────────────────────────────────────────────────────
"But we wanted independent teams!"
Reality:
• Shared component library changes need coordination
• API contracts between MFEs need agreement
• Design system updates affect everyone
• Shell changes need buy-in
• Dependency upgrades need synchronization
You don't eliminate coordination.
You move it to different (often harder) places.
Performance Impact
PERFORMANCE: THE HIDDEN COST
════════════════════════════════════════════════════════════════════
WATERFALL LOADING:
──────────────────
Monolith:
─────────
HTML ─────► Bundle ─────► Render
50ms 200ms 100ms
Total: 350ms
Micro-frontends (naive):
────────────────────────
HTML ─► Shell ─► Determine route ─► Load MFE ─► Render MFE
50ms 150ms 50ms 200ms 100ms
Total: 550ms
Added 200ms just for MFE discovery and loading!
MULTIPLE FRAMEWORK BOOTSTRAPS:
──────────────────────────────
Monolith:
React.createRoot() × 1 = ~10ms
Micro-frontends (different frameworks):
React.createRoot() × 1 = ~10ms (Shell)
React.createRoot() × 1 = ~10ms (MFE-A)
Vue.createApp() × 1 = ~8ms (MFE-B)
React.createRoot() × 1 = ~10ms (MFE-C)
Total bootstrap: ~38ms
Plus hydration, plus state initialization...
RUNTIME OVERHEAD:
─────────────────
Module Federation adds:
• Async chunk loading
• Remote entry fetching
• Shared scope initialization
• Container initialization
Not huge individually, but adds up.
OPTIMIZATION IS HARDER:
───────────────────────
Monolith optimizations:
• Tree shaking (works perfectly)
• Code splitting (straightforward)
• Preloading (predictable)
MFE optimizations:
• Tree shaking (cross-boundary? hard)
• Code splitting (per-MFE already split)
• Preloading (which MFE will user visit?)
Each MFE optimizes locally.
Global optimization requires coordination.
Implementation Approaches
The Options
MICRO-FRONTEND INTEGRATION APPROACHES:
════════════════════════════════════════════════════════════════════
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 1. BUILD-TIME INTEGRATION │
│ ───────────────────────────────────────────────────────────── │
│ MFEs as npm packages, composed at build time. │
│ │
│ npm install @company/search-mfe @company/product-mfe │
│ │
│ Pros: │
│ ✓ Simple mental model │
│ ✓ Single bundle optimization │
│ ✓ Type safety across boundaries │
│ ✓ Standard tooling │
│ │
│ Cons: │
│ ✗ Must rebuild shell to update MFE │
│ ✗ Not truly independent deploys │
│ ✗ Version coordination required │
│ │
│ Verdict: Not really micro-frontends. Just a monorepo. │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 2. IFRAME INTEGRATION │
│ ───────────────────────────────────────────────────────────── │
│ Each MFE in its own iframe. │
│ │
│ <iframe src="https://search.company.com" /> │
│ <iframe src="https://product.company.com" /> │
│ │
│ Pros: │
│ ✓ Complete isolation │
│ ✓ True independence │
│ ✓ Simple integration │
│ ✓ Security (different origins) │
│ │
│ Cons: │
│ ✗ No shared state (without postMessage) │
│ ✗ Styling isolation (can't share CSS) │
│ ✗ Accessibility challenges │
│ ✗ SEO limitations │
│ ✗ Performance overhead │
│ ✗ Feels like 2005 │
│ │
│ Verdict: Use for truly isolated widgets (chat, support). │
│ Not for core app composition. │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 3. WEB COMPONENTS │
│ ───────────────────────────────────────────────────────────── │
│ MFEs as custom elements. │
│ │
│ <search-mfe></search-mfe> │
│ <product-mfe product-id="123"></product-mfe> │
│ │
│ Pros: │
│ ✓ Framework agnostic │
│ ✓ Style encapsulation (Shadow DOM) │
│ ✓ Standard browser API │
│ ✓ Works everywhere │
│ │
│ Cons: │
│ ✗ React/Vue integration quirks │
│ ✗ Server-side rendering is tricky │
│ ✗ Large framework bundles still needed │
│ ✗ Shadow DOM styling limitations │
│ ✗ State sharing still hard │
│ │
│ Verdict: Good for design system components. │
│ Awkward for full micro-frontends. │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 4. MODULE FEDERATION (Webpack 5) │
│ ───────────────────────────────────────────────────────────── │
│ Runtime JavaScript module sharing. │
│ │
│ // Shell loads MFE at runtime │
│ const SearchMFE = React.lazy(() => │
│ import('search/SearchApp') │
│ ); │
│ │
│ Pros: │
│ ✓ True runtime integration │
│ ✓ Shared dependencies (single React) │
│ ✓ Independent deploys │
│ ✓ Good DX with TypeScript support │
│ ✓ Version negotiation │
│ │
│ Cons: │
│ ✗ Webpack-specific (or Vite with plugins) │
│ ✗ Complex configuration │
│ ✗ Runtime errors for version mismatches │
│ ✗ Debugging is harder │
│ ✗ Learning curve │
│ │
│ Verdict: The "right" way if you're doing micro-frontends. │
│ But "right" doesn't mean "simple." │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 5. NATIVE FEDERATION (Framework-native) │
│ ───────────────────────────────────────────────────────────── │
│ Module Federation for Vite, Rspack, etc. │
│ │
│ @originjs/vite-plugin-federation │
│ @module-federation/vite │
│ │
│ Pros: │
│ ✓ Works with modern bundlers │
│ ✓ Similar API to Webpack MF │
│ ✓ Faster builds (Vite) │
│ │
│ Cons: │
│ ✗ Less mature than Webpack MF │
│ ✗ Plugin compatibility varies │
│ ✗ Same complexity as Webpack MF │
│ │
│ Verdict: If you're on Vite, this is your path. │
│ │
└─────────────────────────────────────────────────────────────────┘
Module Federation Deep Dive
// MODULE FEDERATION SETUP:
// ═══════════════════════════════════════════════════════════════
// ─────────────────────────────────────────────────────────────────
// SHELL APPLICATION (Host)
// ─────────────────────────────────────────────────────────────────
// webpack.config.js (Shell)
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
// MFEs this shell can load
search: 'search@https://search.cdn.company.com/remoteEntry.js',
product: 'product@https://product.cdn.company.com/remoteEntry.js',
cart: 'cart@https://cart.cdn.company.com/remoteEntry.js',
},
shared: {
// Dependencies to share (avoid duplicates)
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
'react-router-dom': { singleton: true, requiredVersion: '^6.0.0' },
},
}),
],
};
// Shell App.tsx
import React, { Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// Lazy load remote MFEs
const SearchApp = React.lazy(() => import('search/SearchApp'));
const ProductApp = React.lazy(() => import('product/ProductApp'));
const CartApp = React.lazy(() => import('cart/CartApp'));
function App() {
return (
<BrowserRouter>
<Shell>
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/search/*" element={<SearchApp />} />
<Route path="/product/*" element={<ProductApp />} />
<Route path="/cart/*" element={<CartApp />} />
</Routes>
</Suspense>
</Shell>
</BrowserRouter>
);
}
// ─────────────────────────────────────────────────────────────────
// SEARCH MFE (Remote)
// ─────────────────────────────────────────────────────────────────
// webpack.config.js (Search MFE)
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'search',
filename: 'remoteEntry.js',
exposes: {
// What this MFE exports
'./SearchApp': './src/SearchApp',
'./SearchWidget': './src/components/SearchWidget',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
'react-router-dom': { singleton: true, requiredVersion: '^6.0.0' },
},
}),
],
};
// src/SearchApp.tsx
import { Routes, Route } from 'react-router-dom';
import SearchPage from './pages/SearchPage';
import SearchResults from './pages/SearchResults';
// This MFE manages its own routes under /search/*
export default function SearchApp() {
return (
<Routes>
<Route index element={<SearchPage />} />
<Route path="results" element={<SearchResults />} />
</Routes>
);
}
// ─────────────────────────────────────────────────────────────────
// TYPE DEFINITIONS (for TypeScript)
// ─────────────────────────────────────────────────────────────────
// In shell: src/types/remotes.d.ts
declare module 'search/SearchApp' {
const SearchApp: React.ComponentType;
export default SearchApp;
}
declare module 'search/SearchWidget' {
interface SearchWidgetProps {
onSearch: (query: string) => void;
placeholder?: string;
}
const SearchWidget: React.ComponentType<SearchWidgetProps>;
export default SearchWidget;
}
declare module 'product/ProductApp' {
const ProductApp: React.ComponentType;
export default ProductApp;
}
declare module 'cart/CartApp' {
const CartApp: React.ComponentType;
export default CartApp;
}
Cross-MFE Communication
// CROSS-MFE COMMUNICATION PATTERNS:
// ═══════════════════════════════════════════════════════════════
// ─────────────────────────────────────────────────────────────────
// PATTERN 1: Custom Events (Simple, Decoupled)
// ─────────────────────────────────────────────────────────────────
// Shared event types (in shared package)
// packages/shared-events/src/index.ts
export interface AddToCartEvent {
type: 'cart:add';
payload: {
productId: string;
quantity: number;
};
}
export interface CartUpdatedEvent {
type: 'cart:updated';
payload: {
itemCount: number;
};
}
export type AppEvent = AddToCartEvent | CartUpdatedEvent;
export function emitEvent(event: AppEvent) {
window.dispatchEvent(new CustomEvent(event.type, { detail: event.payload }));
}
export function onEvent<T extends AppEvent>(
type: T['type'],
handler: (payload: T['payload']) => void
) {
const listener = (e: CustomEvent) => handler(e.detail);
window.addEventListener(type, listener as EventListener);
return () => window.removeEventListener(type, listener as EventListener);
}
// In Product MFE: Add to cart
import { emitEvent } from '@company/shared-events';
function ProductPage({ productId }) {
const handleAddToCart = () => {
emitEvent({
type: 'cart:add',
payload: { productId, quantity: 1 },
});
};
return <button onClick={handleAddToCart}>Add to Cart</button>;
}
// In Cart MFE: Listen for adds
import { onEvent } from '@company/shared-events';
function CartProvider({ children }) {
useEffect(() => {
return onEvent('cart:add', ({ productId, quantity }) => {
addToCart(productId, quantity);
});
}, []);
return children;
}
// In Shell: Listen for cart updates (for header badge)
import { onEvent } from '@company/shared-events';
function Header() {
const [cartCount, setCartCount] = useState(0);
useEffect(() => {
return onEvent('cart:updated', ({ itemCount }) => {
setCartCount(itemCount);
});
}, []);
return <span>Cart ({cartCount})</span>;
}
// ─────────────────────────────────────────────────────────────────
// PATTERN 2: Shared State Store (More Coupling, More Features)
// ─────────────────────────────────────────────────────────────────
// packages/shared-state/src/stores/cart.ts
import { create } from 'zustand';
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
itemCount: number;
}
// Single instance shared across MFEs via Module Federation
export const useCartStore = create<CartStore>((set, get) => ({
items: [],
addItem: (item) =>
set((state) => ({
items: [...state.items, item],
itemCount: state.items.length + 1,
})),
removeItem: (id) =>
set((state) => ({
items: state.items.filter((i) => i.id !== id),
itemCount: state.items.length - 1,
})),
get itemCount() {
return get().items.length;
},
}));
// webpack.config.js (Shell) - Share the store
{
shared: {
'@company/shared-state': {
singleton: true,
requiredVersion: '^1.0.0',
},
},
}
// Now any MFE can use:
import { useCartStore } from '@company/shared-state';
function AnyComponent() {
const addItem = useCartStore((s) => s.addItem);
const itemCount = useCartStore((s) => s.itemCount);
// ...
}
// ─────────────────────────────────────────────────────────────────
// PATTERN 3: Props Passed from Shell (Simplest)
// ─────────────────────────────────────────────────────────────────
// Shell passes shared state/functions to MFEs
function App() {
const [user, setUser] = useState(null);
const [cart, setCart] = useState([]);
return (
<Routes>
<Route
path="/product/*"
element={
<ProductApp
user={user}
onAddToCart={(item) => setCart([...cart, item])}
/>
}
/>
</Routes>
);
}
// ProductApp receives props
export default function ProductApp({ user, onAddToCart }) {
// Can use user context and cart actions
}
// Simple but creates tight coupling to shell's interface
When Micro-Frontends Make Sense
The Actual Use Cases
WHEN MICRO-FRONTENDS ARE WORTH IT:
════════════════════════════════════════════════════════════════════
✓ VERY LARGE ORGANIZATIONS (100+ frontend developers)
─────────────────────────────────────────────────────────────────
At scale, coordination cost dominates.
5 developers: "Hey, I'm changing the header"
50 developers: Release trains, merge conflicts, blocked deploys
500 developers: Chaos without boundaries
MFEs let teams work in parallel with clear boundaries.
BUT: Most companies don't have 100 frontend developers.
✓ GENUINELY INDEPENDENT PRODUCT AREAS
─────────────────────────────────────────────────────────────────
Separate products that share a shell.
Example: Enterprise dashboard
├── Analytics (separate team, separate roadmap)
├── Billing (separate team, compliance requirements)
├── Admin (separate team, different release cycle)
└── Settings (shared)
These are almost separate applications.
They share navigation and auth, little else.
✓ LEGACY MIGRATION STRATEGY
─────────────────────────────────────────────────────────────────
Strangling the monolith.
Old app: jQuery + Backbone
Goal: Migrate to React
Strategy:
1. Create React shell
2. Embed old app as MFE (iframe or module)
3. Extract features one by one to new React MFEs
4. Eventually remove old app MFE
MFEs as a migration pattern, not end state.
✓ ACQUISITIONS / MERGERS
─────────────────────────────────────────────────────────────────
Integrating purchased products.
Company buys startup with Vue app.
Need to integrate into existing React platform.
Options:
a) Rewrite startup app in React (expensive, slow)
b) Run as separate product (bad UX)
c) Integrate as MFE (unified UX, keep Vue)
MFEs as integration strategy.
✓ DIFFERENT COMPLIANCE REQUIREMENTS
─────────────────────────────────────────────────────────────────
Parts of app need different security/audit levels.
Example: Healthcare platform
├── Marketing site (public, fast, SEO)
├── Patient portal (HIPAA, strict auditing)
└── Provider dashboard (different access controls)
Isolation boundaries for compliance.
When to Avoid
WHEN MICRO-FRONTENDS ARE NOT WORTH IT:
════════════════════════════════════════════════════════════════════
✗ "OUR MONOLITH IS GETTING BIG"
─────────────────────────────────────────────────────────────────
Real problem: Poor code organization
Big monolith → micro-frontends?
No. Big monolith → well-organized monolith.
Try first:
• Feature folders
• Clear module boundaries
• Lazy loading
• Code splitting
• Monorepo with packages
These solve 90% of "big app" problems without MFE complexity.
✗ "TEAMS KEEP STEPPING ON EACH OTHER"
─────────────────────────────────────────────────────────────────
Real problem: Unclear ownership / bad communication
MFEs won't fix:
• Unclear who owns what code
• No code review standards
• Missing documentation
• Poor git workflows
Fix the process before adding architecture complexity.
✗ "WE WANT TO TRY NEW FRAMEWORKS"
─────────────────────────────────────────────────────────────────
Real problem: Framework FOMO
Multiple frameworks =
• Multiple mental models
• Multiple tooling setups
• Harder hiring
• Inconsistent patterns
• Harder debugging
"Flexibility" in theory, fragmentation in practice.
✗ "IT'S WHAT BIG COMPANIES DO"
─────────────────────────────────────────────────────────────────
Spotify has ~1,000 frontend developers.
You probably don't.
Solutions for 1,000 developers are inappropriate for 10.
Cargo-culting big company architecture is expensive.
✗ "FASTER DEVELOPMENT"
─────────────────────────────────────────────────────────────────
Ironic, because MFEs often slow you down:
• Setup time: weeks to months
• Every cross-MFE feature: coordination
• Debugging: harder
• Testing: more complex
• Build pipelines: more infrastructure
"Independent deployment" doesn't mean "faster overall."
✗ TEAM SIZE < 30 DEVELOPERS
─────────────────────────────────────────────────────────────────
Rules of thumb:
1-10 developers: Monolith, always
10-30 developers: Probably monolith, maybe monorepo
30-100 developers: Consider MFEs if clear boundaries exist
100+ developers: MFEs likely necessary
Below 30, the coordination overhead isn't worth it.
Alternatives to Micro-Frontends
What to Try First
BEFORE MICRO-FRONTENDS, TRY:
════════════════════════════════════════════════════════════════════
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 1. WELL-STRUCTURED MONOLITH │
│ ───────────────────────────────────────────────────────────── │
│ │
│ src/ │
│ ├── features/ # Feature-based organization │
│ │ ├── search/ │
│ │ │ ├── components/ │
│ │ │ ├── hooks/ │
│ │ │ ├── api/ │
│ │ │ └── index.ts # Public API │
│ │ ├── product/ │
│ │ ├── cart/ │
│ │ └── checkout/ │
│ ├── shared/ # Truly shared code │
│ │ ├── components/ │
│ │ ├── hooks/ │
│ │ └── utils/ │
│ └── app/ # App shell, routing │
│ │
│ Rules: │
│ • Features only import from shared/ or their own folder │
│ • No direct imports between features │
│ • Clear ownership (CODEOWNERS file) │
│ • Enforced with eslint-plugin-boundaries │
│ │
│ This gives you 80% of MFE benefits with 20% of complexity. │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 2. MONOREPO WITH PACKAGES │
│ ───────────────────────────────────────────────────────────── │
│ │
│ packages/ │
│ ├── search/ # Own package.json, can be versioned │
│ │ ├── src/ │
│ │ ├── package.json │
│ │ └── tsconfig.json │
│ ├── product/ │
│ ├── cart/ │
│ ├── shared-ui/ # Shared components │
│ └── app/ # Main app, composes packages │
│ │
│ Tools: Turborepo, Nx, pnpm workspaces │
│ │
│ Benefits: │
│ • Clear boundaries enforced by package structure │
│ • Teams can "own" packages │
│ • Faster builds (package-level caching) │
│ • Still one deploy (simpler ops) │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 3. LAZY LOADING + CODE SPLITTING │
│ ───────────────────────────────────────────────────────────── │
│ │
│ const SearchPage = lazy(() => import('./features/search')); │
│ const ProductPage = lazy(() => import('./features/product')); │
│ const CartPage = lazy(() => import('./features/cart')); │
│ │
│ Combined with route-based splitting: │
│ • Each feature loads only when needed │
│ • Initial bundle is small │
│ • Still one app, one deploy │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 4. FEATURE FLAGS + GRADUAL ROLLOUT │
│ ───────────────────────────────────────────────────────────── │
│ │
│ "But we need independent deploys!" │
│ │
│ Feature flags give you: │
│ • Deploy anytime (code is dark until flag enabled) │
│ • Gradual rollout (1% → 10% → 100%) │
│ • Instant rollback (disable flag) │
│ • A/B testing │
│ │
│ if (featureFlags.newCheckout) { │
│ return <NewCheckout />; │
│ } │
│ return <OldCheckout />; │
│ │
│ This solves "independent releases" without MFE complexity. │
│ │
└─────────────────────────────────────────────────────────────────┘
The Progression Path
STATE MANAGEMENT PROGRESSION:
════════════════════════════════════════════════════════════════════
Level 1: SMALL APP (1-5 developers)
───────────────────────────────────
Just build the app. Don't overthink it.
src/
├── components/
├── pages/
├── hooks/
└── utils/
Level 2: GROWING APP (5-15 developers)
──────────────────────────────────────
Feature folders, clear ownership.
src/
├── features/
│ ├── search/ # Team A owns
│ ├── product/ # Team B owns
│ └── cart/ # Team C owns
└── shared/
Level 3: LARGE APP (15-30 developers)
─────────────────────────────────────
Monorepo with packages.
packages/
├── search/
├── product/
├── cart/
├── shared-ui/
└── app/
Still one build, one deploy.
Add feature flags for independent releases.
Level 4: VERY LARGE / SPECIAL REQUIREMENTS (30+ developers)
───────────────────────────────────────────────────────────
NOW consider micro-frontends.
But only if:
• Clear domain boundaries exist
• Teams are truly independent
• Different release cycles needed
• Or: legacy migration scenario
Otherwise, stay at Level 3.
THE KEY INSIGHT:
────────────────
Each level is a response to REAL pain, not anticipated pain.
Don't jump to Level 4 because you think you'll need it.
Start simple. Add complexity when current approach fails.
If You Must: Implementation Checklist
Prerequisites
BEFORE IMPLEMENTING MICRO-FRONTENDS:
════════════════════════════════════════════════════════════════════
ORGANIZATIONAL PREREQUISITES:
─────────────────────────────
□ Clear team boundaries
Each MFE has ONE owning team.
Shared MFEs have clear governance.
□ Strong platform team
Someone owns: shell, shared libraries, CI/CD, infrastructure.
This is a full-time job, not a side project.
□ Design system in place
Shared components, tokens, patterns.
Without this: Frankenstein UI guaranteed.
□ API contracts defined
How MFEs communicate.
Versioning strategy.
□ Deployment infrastructure
Independent pipelines per MFE.
CDN configuration.
Version management.
TECHNICAL PREREQUISITES:
────────────────────────
□ Shared dependency strategy
Which versions of React, Router, etc.?
How to handle version mismatches?
Singleton vs. multiple instances?
□ Authentication / Authorization
How is auth state shared?
Token management across MFEs?
Session synchronization?
□ Routing strategy
Shell routes vs. MFE routes?
Deep linking?
Browser history management?
□ Styling strategy
CSS isolation approach?
Shared design tokens?
Theme propagation?
□ Testing strategy
Unit tests per MFE?
Integration tests across MFEs?
E2E tests?
Contract tests?
□ Error handling
Error boundaries per MFE?
Graceful degradation?
Error reporting/aggregation?
□ Performance monitoring
Metrics per MFE?
Core Web Vitals tracking?
Bundle size budgets?
IF YOU CAN'T CHECK MOST OF THESE:
─────────────────────────────────
You're not ready for micro-frontends.
The implementation will fail or cause more problems than it solves.
Architecture Decisions
KEY ARCHITECTURE DECISIONS:
════════════════════════════════════════════════════════════════════
1. INTEGRATION APPROACH
─────────────────────
Decision: Module Federation vs. Web Components vs. Other
Recommendation:
• Same framework everywhere → Module Federation
• Multiple frameworks necessary → Web Components or iframes
• Simple widget embedding → iframes
2. ROUTING OWNERSHIP
──────────────────
Option A: Shell owns all routing
┌─────────────────────────────────────────────────┐
│ Shell │
│ ├── /search → render SearchMFE │
│ ├── /search/results → render SearchMFE │
│ ├── /product/:id → render ProductMFE │
│ └── /cart → render CartMFE │
└─────────────────────────────────────────────────┘
Option B: Shell owns top-level, MFEs own sub-routes
┌─────────────────────────────────────────────────┐
│ Shell │
│ ├── /search/* → SearchMFE handles internally │
│ ├── /product/* → ProductMFE handles internally │
│ └── /cart/* → CartMFE handles internally │
└─────────────────────────────────────────────────┘
Recommendation: Option B (more autonomy for teams)
3. STATE SHARING
──────────────
Levels of sharing:
None: MFEs don't share state
Communication via URL or backend
Events: Custom events for notifications
Loosely coupled
Shared store: Single store shared via Module Federation
Tightly coupled
Recommendation: Start with events. Add shared store only
for specific high-frequency needs.
4. FAILURE ISOLATION
──────────────────
What happens when ProductMFE fails to load?
Option A: Show error, rest of app works
Option B: Retry with exponential backoff
Option C: Show fallback/cached version
<ErrorBoundary fallback={<ProductMFEError />}>
<Suspense fallback={<ProductSkeleton />}>
<ProductMFE />
</Suspense>
</ErrorBoundary>
5. VERSIONING
───────────
How do you handle MFE version updates?
Option A: Always latest (CDN serves current version)
Option B: Shell requests specific version
Option C: Version manifest (shell fetches manifest first)
// Version manifest approach
{
"search": "https://cdn/search/v2.3.1/remoteEntry.js",
"product": "https://cdn/product/v1.8.0/remoteEntry.js",
"cart": "https://cdn/cart/v3.0.2/remoteEntry.js"
}
Decision Framework
The Decision Tree
SHOULD YOU USE MICRO-FRONTENDS?
════════════════════════════════════════════════════════════════════
START HERE
│
▼
┌───────────────────────────────────┐
│ How many frontend developers? │
└───────────────────────────────────┘
│
├── < 30 ──────────────────────────────────► NO
│ Use monorepo
│
└── 30+ ───┐
│
▼
┌───────────────────────────────────┐
│ Are there clear domain │
│ boundaries between teams? │
└───────────────────────────────────┘
│
├── NO ────────────────────────────────────► NO
│ Define boundaries first
│
└── YES ───┐
│
▼
┌───────────────────────────────────┐
│ Do teams have genuinely │
│ different release cycles? │
└───────────────────────────────────┘
│
├── NO ────────────────────────────────────► PROBABLY NO
│ Feature flags may suffice
│
└── YES ───┐
│
▼
┌───────────────────────────────────┐
│ Have you tried: │
│ • Monorepo with packages? │
│ • Feature flags? │
│ • Code splitting? │
└───────────────────────────────────┘
│
├── NO ────────────────────────────────────► NO
│ Try these first
│
└── YES, still hitting limits ───┐
│
▼
┌───────────────────────────────────┐
│ Do you have: │
│ • Platform team to support? │
│ • Design system in place? │
│ • CI/CD capacity? │
└───────────────────────────────────┘
│
├── NO ────────────────────────────────────► NOT YET
│ Build prerequisites first
│
└── YES ───┐
│
▼
┌───────────────────────────────────────────────────────────────┐
│ │
│ MAYBE micro-frontends are right for you. │
│ │
│ But start small: │
│ • One MFE extracted from monolith │
│ • Validate the approach │
│ • Expand only if it works │
│ │
└───────────────────────────────────────────────────────────────┘
Quick Assessment
MICRO-FRONTEND READINESS SCORECARD:
════════════════════════════════════════════════════════════════════
Score each criteria 0-2:
0 = No/Not at all
1 = Partially/Sometimes
2 = Yes/Definitely
ORGANIZATIONAL FACTORS:
───────────────────────
[ ] 30+ frontend developers ___
[ ] Teams aligned to clear business domains ___
[ ] Teams need different release cycles ___
[ ] Strong platform/infrastructure team exists ___
[ ] Design system is mature and adopted ___
[ ] Leadership understands and supports complexity ___
TECHNICAL FACTORS:
──────────────────
[ ] Current monolith has clear module boundaries ___
[ ] CI/CD can handle multiple pipelines ___
[ ] Monitoring/observability is mature ___
[ ] Testing strategy includes integration tests ___
[ ] Team has Module Federation experience ___
TRIED ALTERNATIVES:
───────────────────
[ ] Monorepo with packages ___
[ ] Feature flags for independent releases ___
[ ] Code splitting and lazy loading ___
[ ] Clear CODEOWNERS and review process ___
TOTAL: ___ / 28
INTERPRETATION:
───────────────
0-10: Definitely not ready. Focus on fundamentals.
11-18: Probably not worth it. Simpler solutions exist.
19-24: Maybe. Start with one MFE as experiment.
25-28: Good candidate. Proceed carefully.
REMEMBER:
─────────
A score of 28 doesn't mean you SHOULD use micro-frontends.
It means you COULD without immediate disaster.
The question remains: Is the complexity worth it?
Quick Reference
┌─────────────────────────────────────────────────────────────────────┐
│ MICRO-FRONTEND QUICK REFERENCE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ WHEN TO USE │
│ ───────────────────────────────────────────────────────────────── │
│ ✓ 100+ frontend developers │
│ ✓ Truly independent product areas │
│ ✓ Legacy migration (strangler pattern) │
│ ✓ Acquisitions requiring integration │
│ ✓ Different compliance requirements │
│ │
│ WHEN NOT TO USE │
│ ───────────────────────────────────────────────────────────────── │
│ ✗ < 30 developers (almost always) │
│ ✗ "Monolith is getting big" (use better organization) │
│ ✗ "Want to try new frameworks" (don't) │
│ ✗ "It's what big companies do" (you're not them) │
│ ✗ "For faster development" (it's usually slower) │
│ │
│ INTEGRATION APPROACHES │
│ ───────────────────────────────────────────────────────────────── │
│ Module Federation │ Best for: Same framework, true independence │
│ Web Components │ Best for: Framework agnostic widgets │
│ Iframes │ Best for: Complete isolation, legacy embed │
│ Build-time │ Best for: Nothing (it's just a monorepo) │
│ │
│ COMPLEXITY ADDED │
│ ───────────────────────────────────────────────────────────────── │
│ • Build: Per-MFE pipelines, version management │
│ • Test: Unit + integration + contract + E2E │
│ • Deploy: Per-MFE CDN, manifest management │
│ • State: Cross-MFE communication │
│ • Style: Design system, isolation │
│ • Auth: Token sharing, session sync │
│ • Perf: Bundle size, load waterfalls │
│ │
│ TRY FIRST (ALTERNATIVES) │
│ ───────────────────────────────────────────────────────────────── │
│ 1. Feature folders with clear ownership │
│ 2. Monorepo with packages (Turborepo, Nx) │
│ 3. Code splitting + lazy loading │
│ 4. Feature flags for independent releases │
│ │
│ PREREQUISITES │
│ ───────────────────────────────────────────────────────────────── │
│ □ Clear team boundaries and ownership │
│ □ Platform team to support infrastructure │
│ □ Design system in place │
│ □ Shared dependency strategy │
│ □ CI/CD capacity for multiple pipelines │
│ □ Testing strategy across MFE boundaries │
│ │
│ RED FLAGS │
│ ───────────────────────────────────────────────────────────────── │
│ ✗ "Let's use different frameworks for flexibility" │
│ ✗ Starting with micro-frontends on new project │
│ ✗ No platform team to maintain infrastructure │
│ ✗ No design system (UI will be inconsistent) │
│ ✗ Extracting based on technical layers, not domains │
│ │
│ TEAM SIZE HEURISTICS │
│ ───────────────────────────────────────────────────────────────── │
│ 1-10 devs: Monolith, always │
│ 10-30 devs: Probably monolith/monorepo │
│ 30-100 devs: Consider if clear boundaries exist │
│ 100+ devs: Likely necessary │
│ │
└─────────────────────────────────────────────────────────────────────┘
Conclusion
Micro-frontends are a solution to a specific problem: coordinating large numbers of developers working on a single user-facing application. They're not a general-purpose architecture improvement.
The honest assessment:
For most teams, micro-frontends add complexity without proportional benefit. The problems they solve—team coordination, independent deployments, code isolation—can usually be addressed with simpler approaches: monorepos, feature flags, code splitting, and clear ownership.
The real questions to ask:
-
Is coordination our bottleneck? If you have 10 developers and deploys are slow, the problem isn't architecture—it's process. Fix your CI/CD, improve your testing, clarify ownership.
-
Have we tried simpler solutions? A well-organized monorepo with feature flags gives you independent releases without runtime integration complexity. Try this first.
-
Do we have the infrastructure team? Micro-frontends require ongoing platform investment. Without a dedicated team maintaining the shell, shared libraries, and deployment infrastructure, it falls apart.
-
Are our domains truly independent? If features constantly need to share state and communicate, you're fighting the architecture. MFEs work best when boundaries are clean.
If you do proceed:
- Start with one MFE extracted from an existing monolith
- Validate the approach works for your team
- Expand only when proven
- Invest heavily in shared infrastructure and design systems
- Accept that development will be slower initially (and maybe permanently)
The bottom line:
Micro-frontends are worth the complexity for large organizations with clear domain boundaries and genuine independence requirements. For everyone else—which is most teams—a well-structured monolith or monorepo is simpler, faster to develop, and easier to maintain.
Don't adopt micro-frontends because big companies use them. Adopt them because you've exhausted simpler alternatives and the coordination benefits outweigh the substantial complexity costs.
When in doubt, stay with the monolith. You can always extract later. You can't easily merge back.
What did you think?