Front-end Engineering Lab
PatternsMicrofrontends

Microfrontend Architecture

Complete guide to designing and implementing microfrontend architectures - patterns, trade-offs, and real-world examples

Microfrontends extend the microservices concept to frontend development, allowing teams to work independently on different parts of a web application.

🎯 What Are Microfrontends?

Definition

Microfrontends are an architectural pattern where a frontend app is decomposed into individual, independent mini-apps that work together to create a unified user experience.

Core Principles

Traditional Monolith:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         One Large App               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚  All features in single repo  β”‚ β”‚
β”‚  β”‚  One deployment               β”‚ β”‚
β”‚  β”‚  One team owns everything     β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Microfrontends:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Shell Application           β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”        β”‚
β”‚  β”‚Team β”‚  β”‚Team β”‚  β”‚Team β”‚        β”‚
β”‚  β”‚  A  β”‚  β”‚  B  β”‚  β”‚  C  β”‚        β”‚
β”‚  β”‚ MFE β”‚  β”‚ MFE β”‚  β”‚ MFE β”‚        β”‚
β”‚  β””β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”˜        β”‚
β”‚    ↓         ↓         ↓           β”‚
β”‚  Deploy  Deploy  Deploy            β”‚
β”‚  Independently                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ—οΈ Architecture Patterns

1. Build-Time Integration (Simplest)

Each microfrontend is a npm package published independently.

// Shell app package.json
{
  "dependencies": {
    "@company/header-mfe": "^1.2.0",
    "@company/products-mfe": "^2.0.1",
    "@company/checkout-mfe": "^1.5.3"
  }
}
// Shell app
import Header from '@company/header-mfe';
import Products from '@company/products-mfe';
import Checkout from '@company/checkout-mfe';

export default function App() {
  return (
    <div>
      <Header />
      <Products />
      <Checkout />
    </div>
  );
}

Pros:

  • βœ… Simple to implement
  • βœ… Type safety
  • βœ… Good for small teams

Cons:

  • ❌ Must rebuild shell for updates
  • ❌ Not truly independent
  • ❌ Version conflicts possible

2. Runtime Integration (Module Federation)

Microfrontends are loaded at runtime using Webpack Module Federation.

Setup

// Header MFE - webpack.config.js
const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'header',
      filename: 'remoteEntry.js',
      exposes: {
        './Header': './src/Header',
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
      },
    }),
  ],
};
// Shell App - webpack.config.js
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      remotes: {
        header: 'header@http://localhost:3001/remoteEntry.js',
        products: 'products@http://localhost:3002/remoteEntry.js',
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
      },
    }),
  ],
};
// Shell App - Load dynamically
import React, { lazy, Suspense } from 'react';

const Header = lazy(() => import('header/Header'));
const Products = lazy(() => import('products/ProductList'));

export default function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <Header />
        <Products />
      </Suspense>
    </div>
  );
}

Pros:

  • βœ… True independence
  • βœ… Deploy without rebuilding shell
  • βœ… Share dependencies
  • βœ… Load on-demand

Cons:

  • ❌ Complex setup
  • ❌ Runtime overhead
  • ❌ Debugging harder

3. iframe Integration (Isolated)

Each microfrontend runs in an iframe.

export default function App() {
  return (
    <div>
      <iframe 
        src="https://header.example.com" 
        style={{ width: '100%', height: '60px', border: 'none' }}
      />
      <iframe 
        src="https://products.example.com" 
        style={{ width: '100%', height: '800px', border: 'none' }}
      />
    </div>
  );
}

Communication Between iframes:

// Parent (Shell)
window.addEventListener('message', (event) => {
  if (event.origin === 'https://products.example.com') {
    if (event.data.type === 'PRODUCT_SELECTED') {
      console.log('Product selected:', event.data.productId);
    }
  }
});

// Child (Products MFE)
window.parent.postMessage({
  type: 'PRODUCT_SELECTED',
  productId: '123'
}, 'https://shell.example.com');

Pros:

  • βœ… Complete isolation (CSS, JS)
  • βœ… Different frameworks
  • βœ… Security boundaries

Cons:

  • ❌ SEO issues
  • ❌ Performance overhead
  • ❌ Complex communication
  • ❌ Routing difficulties

4. Web Components (Standard)

Use native Web Components for each microfrontend.

// Header MFE - header.ts
class HeaderComponent extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `
      <header>
        <nav>
          <a href="/">Home</a>
          <a href="/products">Products</a>
        </nav>
      </header>
    `;
  }
}

customElements.define('app-header', HeaderComponent);
<!-- Shell App -->
<html>
  <body>
    <script src="https://header.cdn.com/header.js"></script>
    <script src="https://products.cdn.com/products.js"></script>
    
    <app-header></app-header>
    <app-products></app-products>
  </body>
</html>

Pros:

  • βœ… Framework agnostic
  • βœ… Browser standard
  • βœ… Shadow DOM isolation

Cons:

  • ❌ Limited React integration
  • ❌ SEO challenges
  • ❌ Complexity with state

πŸ”„ Routing Strategies

Shell owns the router and decides which MFE to render.

// Shell App
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';

const Header = lazy(() => import('header/Header'));
const Products = lazy(() => import('products/App'));
const Checkout = lazy(() => import('checkout/App'));

export default function Shell() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Loading...</div>}>
        <Header />
        <Routes>
          <Route path="/products/*" element={<Products />} />
          <Route path="/checkout/*" element={<Checkout />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}
// Products MFE - Internal routing
import { Routes, Route } from 'react-router-dom';

export default function ProductsApp() {
  return (
    <Routes>
      <Route path="/" element={<ProductList />} />
      <Route path="/:id" element={<ProductDetail />} />
    </Routes>
  );
}

2. Distributed Routing

Each MFE manages its own routes.

// Shell App
export default function Shell() {
  const [currentPath, setCurrentPath] = useState(window.location.pathname);
  
  useEffect(() => {
    const handler = () => setCurrentPath(window.location.pathname);
    window.addEventListener('popstate', handler);
    return () => window.removeEventListener('popstate', handler);
  }, []);
  
  return (
    <div>
      <Header />
      {currentPath.startsWith('/products') && <ProductsMFE />}
      {currentPath.startsWith('/checkout') && <CheckoutMFE />}
    </div>
  );
}

πŸ“¦ Shared Dependencies

Problem: Bundle Duplication

Without sharing:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Header MFE                      β”‚
β”‚ β”œβ”€ React (42KB)                 β”‚
β”‚ β”œβ”€ ReactDOM (130KB)             β”‚
β”‚ └─ App code (20KB)              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Products MFE                    β”‚
β”‚ β”œβ”€ React (42KB)     ← DUPLICATE β”‚
β”‚ β”œβ”€ ReactDOM (130KB) ← DUPLICATE β”‚
β”‚ └─ App code (80KB)              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Total: 444KB (with duplication)

Solution: Shared Dependencies

// Both MFEs
new ModuleFederationPlugin({
  shared: {
    react: {
      singleton: true,      // Only one instance
      requiredVersion: '^18.0.0',
      eager: false,         // Load on-demand
    },
    'react-dom': {
      singleton: true,
      requiredVersion: '^18.0.0',
    },
  },
});
With sharing:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Shared Layer                    β”‚
β”‚ β”œβ”€ React (42KB)                 β”‚
β”‚ └─ ReactDOM (130KB)             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         ↑           ↑
         β”‚           β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Header MFE β”‚  β”‚ Products    β”‚
β”‚ (20KB)     β”‚  β”‚ MFE (80KB)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Total: 272KB (38% smaller!)

πŸ” CSS Isolation

Problem: Style Conflicts

/* Header MFE */
.button {
  background: blue;
  padding: 10px;
}

/* Products MFE */
.button {
  background: red;  /* ❌ Conflicts! */
  padding: 20px;
}

Solution 1: CSS Modules

// Header MFE
import styles from './Header.module.css';

export default function Header() {
  return <button className={styles.button}>Click</button>;
}
// Generates: .Header_button__a1b2c3

Solution 2: CSS-in-JS with Scoping

import styled from 'styled-components';

// Automatically scoped
const Button = styled.button`
  background: blue;
  padding: 10px;
`;

Solution 3: Shadow DOM (Web Components)

class HeaderComponent extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        .button { background: blue; }
      </style>
      <button class="button">Click</button>
    `;
  }
}

Solution 4: Naming Conventions

/* All Header styles prefixed */
.header-button { }
.header-nav { }
.header-logo { }

/* All Products styles prefixed */
.products-button { }
.products-card { }

πŸ”„ State Management

Each MFE manages its own state.

// Products MFE
function ProductsMFE() {
  const [products, setProducts] = useState([]);
  const [cart, setCart] = useState([]);
  
  return (
    <ProductsContext.Provider value={{ products, cart }}>
      <ProductList />
    </ProductsContext.Provider>
  );
}

2. Shared State via Events

Communication through custom events.

// Custom Event Bus
class EventBus {
  private events: Map<string, Function[]> = new Map();
  
  subscribe(event: string, callback: Function) {
    if (!this.events.has(event)) {
      this.events.set(event, []);
    }
    this.events.get(event)!.push(callback);
  }
  
  publish(event: string, data: any) {
    const callbacks = this.events.get(event) || [];
    callbacks.forEach(cb => cb(data));
  }
}

export const eventBus = new EventBus();
// Products MFE - Publish
import { eventBus } from '@company/shared';

function addToCart(product) {
  eventBus.publish('cart:item-added', { product });
}

// Header MFE - Subscribe
import { eventBus } from '@company/shared';

useEffect(() => {
  eventBus.subscribe('cart:item-added', (data) => {
    setCartCount(prev => prev + 1);
  });
}, []);

3. Shared State Store (Use Sparingly)

// Shared store for truly global state
import { create } from 'zustand';

export const useSharedStore = create((set) => ({
  user: null,
  setUser: (user) => set({ user }),
  
  theme: 'light',
  setTheme: (theme) => set({ theme }),
}));
// Any MFE can use it
import { useSharedStore } from '@company/shared';

function Header() {
  const { user, theme } = useSharedStore();
  return <div className={theme}>Welcome {user?.name}</div>;
}

πŸš€ Deployment Strategies

1. Independent Deployments

Each MFE deployed separately.

# Header MFE - GitHub Actions
name: Deploy Header
on:
  push:
    paths:
      - 'packages/header/**'
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - run: npm run build
      - run: aws s3 sync dist/ s3://cdn.example.com/header/
CDN Structure:
https://cdn.example.com/
β”œβ”€β”€ header/
β”‚   β”œβ”€β”€ v1.2.0/
β”‚   β”‚   └── remoteEntry.js
β”‚   └── latest/
β”‚       └── remoteEntry.js  β†’ v1.2.0
β”œβ”€β”€ products/
β”‚   β”œβ”€β”€ v2.1.3/
β”‚   └── latest/
└── checkout/
    β”œβ”€β”€ v1.0.5/
    └── latest/

2. Versioned Deployments

Keep multiple versions for rollback.

// Shell dynamically loads versions
const config = {
  header: 'https://cdn.example.com/header/v1.2.0/remoteEntry.js',
  products: 'https://cdn.example.com/products/v2.1.3/remoteEntry.js',
};

// Easy rollback: just change version
const config = {
  header: 'https://cdn.example.com/header/v1.1.9/remoteEntry.js', // Rolled back
  products: 'https://cdn.example.com/products/v2.1.3/remoteEntry.js',
};

3. Canary Deployments

Gradually roll out new versions.

function getRemoteUrl(mfeName: string) {
  const userId = getCurrentUserId();
  const isCanary = userId % 100 < 10; // 10% of users
  
  const version = isCanary ? 'v2.0.0-canary' : 'v1.9.0';
  return `https://cdn.example.com/${mfeName}/${version}/remoteEntry.js`;
}

⚠️ Error Boundaries

Prevent one MFE from crashing the entire app.

// Shell App
import { ErrorBoundary } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>Something went wrong in this section:</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

export default function Shell() {
  return (
    <div>
      <Header />
      
      <ErrorBoundary FallbackComponent={ErrorFallback}>
        <Suspense fallback={<div>Loading Products...</div>}>
          <ProductsMFE />
        </Suspense>
      </ErrorBoundary>
      
      <ErrorBoundary FallbackComponent={ErrorFallback}>
        <Suspense fallback={<div>Loading Checkout...</div>}>
          <CheckoutMFE />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

πŸ“Š When to Use Microfrontends

βœ… Good Use Cases

Large Organizations

Scenario: 50+ frontend developers
Problem: Coordination nightmare
Solution: 5 teams Γ— 10 devs, each owns MFE
Result: Teams move independently

Different Release Cycles

Team A: Releases daily (experiments)
Team B: Releases monthly (stable features)
Solution: A and B are separate MFEs
Result: No blocking between teams

Legacy Migration

Problem: Rewrite monolith incrementally
Solution: New features as MFEs
Old features: Gradually migrate
Result: No "big bang" rewrite

Different Tech Stacks

Header: Vue.js (legacy)
Products: React (new)
Checkout: Svelte (experiment)
Solution: Each MFE uses own framework

❌ Bad Use Cases

Small Teams

❌ 3 developers
❌ One product
β†’ Overhead > Benefits
β†’ Use monolith

Tight Coupling

❌ Features share lots of state
❌ Constant communication needed
β†’ Can't truly separate
β†’ Use monolith with modules

Simple Applications

❌ 10 pages total
❌ Single team
β†’ Unnecessary complexity
β†’ Use regular app

🎯 Decision Framework

Questions to Ask:

  1. Team Size: > 20 developers? β†’ Consider MFE
  2. Independence: Teams can work without coordination? β†’ Consider MFE
  3. Deploy Frequency: Different cadences? β†’ Consider MFE
  4. Legacy: Gradual migration needed? β†’ Consider MFE
  5. Complexity: Can handle distributed systems? β†’ Consider MFE

Trade-offs Table

AspectMonolithMicrofrontends
Initial SetupSimple βœ…Complex ❌
Team ScalabilityHard ❌Easy βœ…
PerformanceFast βœ…Overhead ⚠️
ConsistencyEasy βœ…Hard ❌
TestingSimple βœ…Complex ❌
DebuggingEasy βœ…Hard ❌
Bundle SizeOptimized βœ…Duplicates ⚠️
IndependenceLow ❌High βœ…

🏒 Real-World Examples

Spotify

Shell: Navigation + Player
β”œβ”€ Home MFE (Team Discovery)
β”œβ”€ Search MFE (Team Search)
β”œβ”€ Library MFE (Team Library)
└─ Playlist MFE (Team Playlists)

Deploy independently
Share design system
Event-based communication

Zalando

Shell: Header + Footer
β”œβ”€ Product Catalog (Team Catalog)
β”œβ”€ Shopping Cart (Team Cart)
β”œβ”€ Checkout (Team Payments)
└─ Account (Team Customer)

iframe-based isolation
Different tech stacks allowed
Gradual migration from monolith

Amazon

Shell: Top navigation
β”œβ”€ Product Page (Team PDP)
β”œβ”€ Buy Box (Team Buy)
β”œβ”€ Reviews (Team Reviews)
└─ Recommendations (Team Rec)

Each widget is a MFE
Independent optimization
A/B testing per MFE

πŸ“š Resources

Tools

  • Module Federation: Webpack 5, Vite, Rspack
  • Single-SPA: Framework for orchestrating MFEs
  • Bit: Component-driven development
  • Nx: Monorepo tools for MFEs

Further Reading


πŸŽ“ Key Takeaways

  1. Microfrontends = Microservices for frontend
  2. Multiple patterns exist (Module Federation, iframes, Web Components)
  3. Best for large teams with independent release cycles
  4. Trade complexity for independence
  5. Not a silver bullet - evaluate carefully
  6. Start simple - migrate incrementally if needed

Microfrontends solve organizational problems, not technical ones. Use them when team scaling is the bottleneck, not when building faster is the goal.

On this page