Micro frontends apply microservices principles to frontend development—breaking monolithic UIs into independently deployable pieces owned by different teams. Here's when this makes sense and how to implement it.
What Are Micro Frontends?#
Monolithic Frontend#
Single codebase, single build, single deployment
├── Header (Team A modified)
├── Product Catalog (Team B owns)
├── Shopping Cart (Team C owns)
└── Checkout (Team B modified)
Problems:
- Coordination overhead
- Deployment coupling
- Scaling teams is hard
- Technology lock-in
Micro Frontend Architecture#
Independent apps, independent builds, independent deployments
├── Shell App (Platform Team)
│ ├── Header MFE (Team A)
│ ├── Catalog MFE (Team B)
│ ├── Cart MFE (Team C)
│ └── Checkout MFE (Team D)
Benefits:
- Independent deployments
- Team autonomy
- Technology diversity
- Incremental upgrades
When to Use Micro Frontends#
Good Fit#
✓ Large organization (5+ frontend teams)
✓ Different teams own different features
✓ Need independent deployment cycles
✓ Diverse technology requirements
✓ Gradual migration of legacy systems
Poor Fit#
✗ Small teams (< 5 developers)
✗ Single-page applications with tight coupling
✗ No clear domain boundaries
✗ Need maximum performance
✗ Simple content sites
Integration Patterns#
Build-Time Integration#
1// package.json
2{
3 "dependencies": {
4 "@team-a/header": "^1.0.0",
5 "@team-b/catalog": "^2.1.0",
6 "@team-c/cart": "^1.5.0"
7 }
8}1// Shell application
2import Header from '@team-a/header';
3import Catalog from '@team-b/catalog';
4import Cart from '@team-c/cart';
5
6function App() {
7 return (
8 <>
9 <Header />
10 <main>
11 <Catalog />
12 <Cart />
13 </main>
14 </>
15 );
16}Pros: Simple, type-safe, optimized bundle Cons: Requires shell rebuild for updates, version coupling
Runtime Integration (Module Federation)#
1// webpack.config.js (Shell)
2const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
3
4module.exports = {
5 plugins: [
6 new ModuleFederationPlugin({
7 name: 'shell',
8 remotes: {
9 header: 'header@https://header.example.com/remoteEntry.js',
10 catalog: 'catalog@https://catalog.example.com/remoteEntry.js',
11 cart: 'cart@https://cart.example.com/remoteEntry.js',
12 },
13 shared: ['react', 'react-dom'],
14 }),
15 ],
16};1// Dynamic loading in shell
2const Header = React.lazy(() => import('header/Header'));
3const Catalog = React.lazy(() => import('catalog/Catalog'));
4const Cart = React.lazy(() => import('cart/Cart'));
5
6function App() {
7 return (
8 <Suspense fallback={<Loading />}>
9 <Header />
10 <main>
11 <Catalog />
12 <Cart />
13 </main>
14 </Suspense>
15 );
16}Pros: True independence, runtime updates, partial failures Cons: Runtime overhead, complexity, shared dependency coordination
Web Components#
1// Team A: header-component.js
2class HeaderComponent extends HTMLElement {
3 connectedCallback() {
4 this.innerHTML = `<header>...</header>`;
5 }
6}
7customElements.define('app-header', HeaderComponent);1<!-- Shell -->
2<script src="https://header.example.com/header-component.js" async></script>
3<script src="https://catalog.example.com/catalog-component.js" async></script>
4
5<app-header></app-header>
6<app-catalog></app-catalog>
7<app-cart></app-cart>Pros: Framework agnostic, native browser support Cons: Limited styling options, SSR challenges
iframes#
<iframe src="https://catalog.example.com" />Pros: Complete isolation Cons: Poor UX, SEO issues, communication overhead
Module Federation Deep Dive#
Exposing Components#
1// webpack.config.js (Catalog MFE)
2module.exports = {
3 plugins: [
4 new ModuleFederationPlugin({
5 name: 'catalog',
6 filename: 'remoteEntry.js',
7 exposes: {
8 './ProductList': './src/components/ProductList',
9 './ProductDetail': './src/components/ProductDetail',
10 './SearchBar': './src/components/SearchBar',
11 },
12 shared: {
13 react: { singleton: true, requiredVersion: '^18.0.0' },
14 'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
15 'react-router-dom': { singleton: true },
16 },
17 }),
18 ],
19};Shared State#
1// shared-state package (consumed by all MFEs)
2import { create } from 'zustand';
3
4export interface User {
5 id: string;
6 name: string;
7 cart: CartItem[];
8}
9
10export const useUserStore = create<UserState>((set) => ({
11 user: null,
12 setUser: (user) => set({ user }),
13 addToCart: (item) => set((state) => ({
14 user: {
15 ...state.user,
16 cart: [...state.user.cart, item],
17 },
18 })),
19}));1// Module federation shared config
2shared: {
3 'shared-state': {
4 singleton: true,
5 eager: true,
6 },
7}Communication Between MFEs#
1// Custom events (framework agnostic)
2// Catalog MFE
3function handleProductClick(product: Product) {
4 window.dispatchEvent(
5 new CustomEvent('product:selected', { detail: product })
6 );
7}
8
9// Cart MFE
10useEffect(() => {
11 const handler = (e: CustomEvent) => {
12 addToCart(e.detail);
13 };
14 window.addEventListener('product:selected', handler);
15 return () => window.removeEventListener('product:selected', handler);
16}, []);Routing#
Shell-Owned Routing#
1// Shell handles all routing
2import { Routes, Route } from 'react-router-dom';
3
4function App() {
5 return (
6 <Routes>
7 <Route path="/" element={<Home />} />
8 <Route path="/products/*" element={<CatalogMFE />} />
9 <Route path="/cart/*" element={<CartMFE />} />
10 <Route path="/checkout/*" element={<CheckoutMFE />} />
11 </Routes>
12 );
13}MFE-Owned Routing#
1// Each MFE handles its own routes
2// Catalog MFE
3function CatalogRoutes() {
4 return (
5 <Routes>
6 <Route path="/" element={<ProductList />} />
7 <Route path="/:productId" element={<ProductDetail />} />
8 <Route path="/category/:category" element={<CategoryPage />} />
9 </Routes>
10 );
11}Styling Strategies#
CSS Modules / Scoped Styles#
/* catalog.module.css */
.productCard {
/* Scoped to this component */
}CSS-in-JS with Namespacing#
import styled from 'styled-components';
const ProductCard = styled.div`
/* Isolated by generated class names */
`;Shadow DOM#
1class CatalogComponent extends HTMLElement {
2 constructor() {
3 super();
4 this.attachShadow({ mode: 'open' });
5 }
6
7 connectedCallback() {
8 this.shadowRoot.innerHTML = `
9 <style>
10 .product { /* Isolated styles */ }
11 </style>
12 <div class="product">...</div>
13 `;
14 }
15}Testing Micro Frontends#
Unit Testing (Same as Before)#
1// Each MFE has its own tests
2import { render, screen } from '@testing-library/react';
3import ProductCard from './ProductCard';
4
5test('displays product name', () => {
6 render(<ProductCard product={{ name: 'Test' }} />);
7 expect(screen.getByText('Test')).toBeInTheDocument();
8});Integration Testing#
1// Test MFE integration in shell
2describe('Shell Integration', () => {
3 it('loads catalog MFE', async () => {
4 render(<App />);
5 // Wait for remote module to load
6 await waitFor(() => {
7 expect(screen.getByTestId('catalog')).toBeInTheDocument();
8 });
9 });
10});Contract Testing#
1// Ensure MFE exposes expected interface
2describe('Catalog MFE Contract', () => {
3 it('exposes ProductList component', async () => {
4 const module = await import('catalog/ProductList');
5 expect(module.default).toBeDefined();
6 expect(typeof module.default).toBe('function');
7 });
8});Performance Considerations#
Bundle Size#
1// Aggressive code splitting
2const ProductDetail = lazy(() =>
3 import(/* webpackChunkName: "product-detail" */ './ProductDetail')
4);
5
6// Shared dependencies reduce duplication
7shared: {
8 react: { singleton: true },
9 lodash: { singleton: true },
10}Loading Strategy#
1// Progressive loading with skeletons
2function App() {
3 return (
4 <>
5 {/* Critical MFE loads first */}
6 <Suspense fallback={<HeaderSkeleton />}>
7 <Header />
8 </Suspense>
9
10 {/* Below-fold MFEs lazy load */}
11 <Suspense fallback={<CatalogSkeleton />}>
12 <Catalog />
13 </Suspense>
14 </>
15 );
16}Deployment#
Independent Deployments#
1# Each MFE has its own pipeline
2# catalog-mfe/.github/workflows/deploy.yml
3name: Deploy Catalog MFE
4
5on:
6 push:
7 branches: [main]
8
9jobs:
10 deploy:
11 steps:
12 - uses: actions/checkout@v4
13 - run: npm ci
14 - run: npm run build
15 - run: aws s3 sync dist/ s3://catalog-mfe/
16 - run: aws cloudfront create-invalidation --distribution-id ${{ secrets.CF_DIST }}Version Management#
// remoteEntry.js includes version
// Shell can request specific versions
remotes: {
catalog: `catalog@https://mfe.example.com/catalog/v${CATALOG_VERSION}/remoteEntry.js`,
}Conclusion#
Micro frontends solve organizational scaling problems—enabling independent teams to work on independent codebases with independent deployments. But they add complexity that isn't worth it for smaller teams.
Evaluate honestly: if your frontend team is under 10 people and you don't have clear domain boundaries, a well-structured monolith is probably better. But if you're at the scale where team coordination is the bottleneck, micro frontends can unlock significant velocity.
Start with module federation, establish clear contracts between MFEs, and invest in shared tooling. The architecture complexity is justified by team independence.