Micro-Frontends: The Complete Architecture Guide for 2026
AI & Development

Micro-Frontends: The Complete Architecture Guide for 2026

Learn how to build scalable micro-frontend applications. Complete guide covering Module Federation, routing, state management, and deployment strategies.

Feb 03, 2026
15 min read
Micro-Frontends: The Complete Architecture Guide for 2026

Micro-frontends promise to solve the scaling challenges of large frontend applications. After implementing dozens of micro-frontend architectures, here's what actually works, what doesn't, and how to build them right.

Related reading: Check out our guides on React performance optimization and design systems at scale for more architecture insights.

What Are Micro-Frontends?#

The Core Concept#

Micro-frontends extend the microservices concept to frontend development. Instead of a monolithic frontend application, you build multiple smaller, independent applications that work together as a cohesive user experience.

Key principles:

  • Technology agnostic: Each micro-frontend can use different frameworks
  • Independent deployment: Deploy parts of your app independently
  • Team autonomy: Different teams own different parts of the application
  • Isolated development: Develop and test in isolation

When Micro-Frontends Make Sense#

Large organizations with multiple teams working on the same product Legacy modernization where you need to gradually migrate old systems Different technology requirements for different parts of your application Independent release cycles for different features

When to Avoid Micro-Frontends#

Small teams (< 10 developers) - the overhead isn't worth it Simple applications - monoliths are often better for straightforward apps Tight coupling requirements - when features need deep integration Performance-critical applications - the overhead can impact performance

Micro-Frontend Implementation Strategies#

1. Module Federation (Webpack 5)#

Best for: React/Vue/Angular applications with modern build tools

// Host application webpack config
const ModuleFederationPlugin = require('@module-federation/webpack');

module.exports = {
  mode: 'development',
  devServer: {
    port: 3000,
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        mfShell: 'shell@http://localhost:3001/remoteEntry.js',
        mfProducts: 'products@http://localhost:3002/remoteEntry.js',
        mfCheckout: 'checkout@http://localhost:3003/remoteEntry.js',
      },
    }),
  ],
};

// Remote application webpack config
module.exports = {
  mode: 'development',
  devServer: {
    port: 3001,
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      filename: 'remoteEntry.js',
      exposes: {
        './Header': './src/components/Header',
        './Navigation': './src/components/Navigation',
        './Footer': './src/components/Footer',
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
      },
    }),
  ],
};

Using remote components:

// Host application
import React, { Suspense } from 'react';

const Header = React.lazy(() => import('mfShell/Header'));
const ProductList = React.lazy(() => import('mfProducts/ProductList'));
const Checkout = React.lazy(() => import('mfCheckout/CheckoutForm'));

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading header...</div>}>
        <Header />
      </Suspense>
      
      <main>
        <Suspense fallback={<div>Loading products...</div>}>
          <ProductList />
        </Suspense>
        
        <Suspense fallback={<div>Loading checkout...</div>}>
          <Checkout />
        </Suspense>
      </main>
    </div>
  );
}

2. Single-SPA Framework#

Best for: Multi-framework applications or gradual migration

// Root config
import { registerApplication, start } from 'single-spa';

// Register micro-frontends
registerApplication({
  name: 'navbar',
  app: () => import('./navbar/navbar.app.js'),
  activeWhen: () => true, // Always active
});

registerApplication({
  name: 'products',
  app: () => import('./products/products.app.js'),
  activeWhen: location => location.pathname.startsWith('/products'),
});

registerApplication({
  name: 'checkout',
  app: () => import('./checkout/checkout.app.js'),
  activeWhen: '/checkout',
});

start();

// Individual micro-frontend (React)
import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import ProductApp from './ProductApp';

const lifecycles = singleSpaReact({
  React,
  ReactDOM,
  rootComponent: ProductApp,
  errorBoundary(err, info, props) {
    return <div>Error in products app</div>;
  },
});

export const { bootstrap, mount, unmount } = lifecycles;

// Individual micro-frontend (Vue)
import Vue from 'vue';
import singleSpaVue from 'single-spa-vue';
import CheckoutApp from './CheckoutApp.vue';

const vueLifecycles = singleSpaVue({
  Vue,
  appOptions: {
    render: h => h(CheckoutApp),
  },
});

export const { bootstrap, mount, unmount } = vueLifecycles;

3. Web Components Approach#

Best for: Framework-agnostic solutions with maximum isolation

// Micro-frontend as Web Component
class ProductCatalog extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }
  
  connectedCallback() {
    this.render();
    this.loadProducts();
  }
  
  render() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          padding: 20px;
        }
        .product-grid {
          display: grid;
          grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
          gap: 20px;
        }
      </style>
      <div class="product-catalog">
        <h2>Products</h2>
        <div class="product-grid" id="products"></div>
      </div>
    `;
  }
  
  async loadProducts() {
    const response = await fetch('/api/products');
    const products = await response.json();
    this.renderProducts(products);
  }
  
  renderProducts(products) {
    const grid = this.shadowRoot.getElementById('products');
    grid.innerHTML = products.map(product => `
      <div class="product-card">
        <h3>${product.name}</h3>
        <p>$${product.price}</p>
        <button onclick="this.addToCart('${product.id}')">Add to Cart</button>
      </div>
    `).join('');
  }
  
  addToCart(productId) {
    // Dispatch custom event for communication
    this.dispatchEvent(new CustomEvent('add-to-cart', {
      detail: { productId },
      bubbles: true,
    }));
  }
}

customElements.define('product-catalog', ProductCatalog);

// Usage in host application
document.addEventListener('add-to-cart', (event) => {
  console.log('Product added to cart:', event.detail.productId);
  // Update cart state
});

4. Server-Side Composition#

Best for: SEO-critical applications with server-side rendering

// Express.js composition server
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();

// Proxy to micro-frontends
app.use('/products', createProxyMiddleware({
  target: 'http://products-service:3001',
  changeOrigin: true,
}));

app.use('/checkout', createProxyMiddleware({
  target: 'http://checkout-service:3002',
  changeOrigin: true,
}));

// Server-side composition
app.get('/', async (req, res) => {
  try {
    // Fetch fragments from micro-frontends
    const [header, products, footer] = await Promise.all([
      fetch('http://shell-service:3000/header').then(r => r.text()),
      fetch('http://products-service:3001/featured').then(r => r.text()),
      fetch('http://shell-service:3000/footer').then(r => r.text()),
    ]);
    
    const html = `
      <!DOCTYPE html>
      <html>
        <head>
          <title>E-commerce App</title>
          <link rel="stylesheet" href="/styles.css">
        </head>
        <body>
          ${header}
          <main>
            ${products}
          </main>
          ${footer}
          <script src="/app.js"></script>
        </body>
      </html>
    `;
    
    res.send(html);
  } catch (error) {
    res.status(500).send('Error loading page');
  }
});

app.listen(3000);

Communication Between Micro-Frontends#

// Publishing events
class EventBus {
  static dispatch(eventName, data) {
    const event = new CustomEvent(eventName, {
      detail: data,
      bubbles: true,
    });
    document.dispatchEvent(event);
  }
  
  static subscribe(eventName, callback) {
    document.addEventListener(eventName, callback);
    
    // Return unsubscribe function
    return () => document.removeEventListener(eventName, callback);
  }
}

// Micro-frontend A (Products)
function addToCart(product) {
  EventBus.dispatch('cart:add', { product });
}

// Micro-frontend B (Cart)
EventBus.subscribe('cart:add', (event) => {
  const { product } = event.detail;
  updateCartState(product);
});

2. Shared State Management#

// Shared store using RxJS
import { BehaviorSubject } from 'rxjs';

class SharedStore {
  constructor() {
    this.state$ = new BehaviorSubject({
      user: null,
      cart: [],
      theme: 'light',
    });
  }
  
  getState() {
    return this.state$.value;
  }
  
  setState(newState) {
    this.state$.next({ ...this.getState(), ...newState });
  }
  
  subscribe(callback) {
    return this.state$.subscribe(callback);
  }
  
  // Specific actions
  addToCart(product) {
    const currentState = this.getState();
    this.setState({
      cart: [...currentState.cart, product],
    });
  }
  
  setUser(user) {
    this.setState({ user });
  }
}

// Global instance
window.sharedStore = new SharedStore();

// Usage in micro-frontends
const store = window.sharedStore;

// Subscribe to changes
store.subscribe((state) => {
  console.log('State updated:', state);
  updateUI(state);
});

// Update state
store.addToCart(product);

3. URL-Based Communication#

// Router service for micro-frontends
class MicroFrontendRouter {
  constructor() {
    this.routes = new Map();
    this.currentRoute = null;
    
    window.addEventListener('popstate', this.handleRouteChange.bind(this));
  }
  
  register(pattern, microfrontend) {
    this.routes.set(pattern, microfrontend);
  }
  
  navigate(path, state = {}) {
    history.pushState(state, '', path);
    this.handleRouteChange();
  }
  
  handleRouteChange() {
    const path = window.location.pathname;
    
    for (const [pattern, microfrontend] of this.routes) {
      if (this.matchRoute(pattern, path)) {
        this.activateMicrofrontend(microfrontend, path);
        break;
      }
    }
  }
  
  matchRoute(pattern, path) {
    const regex = new RegExp(pattern.replace(/:\w+/g, '([^/]+)'));
    return regex.test(path);
  }
  
  activateMicrofrontend(microfrontend, path) {
    if (this.currentRoute !== microfrontend) {
      this.currentRoute?.deactivate?.();
      microfrontend.activate(path);
      this.currentRoute = microfrontend;
    }
  }
}

// Usage
const router = new MicroFrontendRouter();

router.register('/products/:category?', {
  activate: (path) => {
    import('./products/app.js').then(app => app.mount());
  },
  deactivate: () => {
    // Cleanup
  },
});

router.register('/checkout', {
  activate: () => {
    import('./checkout/app.js').then(app => app.mount());
  },
});

Styling and Design Systems#

CSS Isolation Strategies#

/* BEM methodology for namespace isolation */
.mf-products__card {
  border: 1px solid #ddd;
  padding: 16px;
}

.mf-products__card--featured {
  border-color: #007bff;
}

/* CSS Modules */
.productCard {
  composes: card from 'shared-styles/components.css';
  border-radius: 8px;
}

/* Styled Components with namespace */
const ProductCard = styled.div`
  border: 1px solid #ddd;
  padding: 16px;
  
  &.mf-products-featured {
    border-color: #007bff;
  }
`;

Shared Design System#

// Design system package
// packages/design-system/src/index.js
export { Button } from './components/Button';
export { Card } from './components/Card';
export { theme } from './theme';

// Micro-frontend usage
import { Button, Card, theme } from '@company/design-system';
import { ThemeProvider } from 'styled-components';

function ProductApp() {
  return (
    <ThemeProvider theme={theme}>
      <Card>
        <h2>Product Name</h2>
        <Button variant="primary">Add to Cart</Button>
      </Card>
    </ThemeProvider>
  );
}

Testing Micro-Frontends#

Unit Testing#

// Jest configuration for micro-frontend
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
  moduleNameMapping: {
    '^@shared/(.*)$': '<rootDir>/../shared/src/$1',
  },
  transform: {
    '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
  },
};

// Testing with mocked dependencies
import { render, screen } from '@testing-library/react';
import ProductList from './ProductList';

// Mock external micro-frontend
jest.mock('mfCart/CartService', () => ({
  addToCart: jest.fn(),
}));

test('renders product list', () => {
  render(<ProductList />);
  expect(screen.getByText('Products')).toBeInTheDocument();
});

Integration Testing#

// Cypress integration tests
describe('Micro-frontend Integration', () => {
  it('should communicate between products and cart', () => {
    cy.visit('/');
    
    // Interact with products micro-frontend
    cy.get('[data-testid="product-card"]').first().click();
    cy.get('[data-testid="add-to-cart"]').click();
    
    // Verify cart micro-frontend updates
    cy.get('[data-testid="cart-count"]').should('contain', '1');
    
    // Navigate to cart
    cy.get('[data-testid="cart-link"]').click();
    cy.url().should('include', '/cart');
    cy.get('[data-testid="cart-item"]').should('exist');
  });
});

Contract Testing#

// Pact.js for API contract testing
import { Pact } from '@pact-foundation/pact';

const provider = new Pact({
  consumer: 'products-microfrontend',
  provider: 'products-api',
  port: 1234,
});

describe('Products API Contract', () => {
  beforeAll(() => provider.setup());
  afterAll(() => provider.finalize());
  
  it('should get product list', async () => {
    await provider.addInteraction({
      state: 'products exist',
      uponReceiving: 'a request for products',
      withRequest: {
        method: 'GET',
        path: '/api/products',
      },
      willRespondWith: {
        status: 200,
        body: [
          { id: 1, name: 'Product 1', price: 99.99 },
        ],
      },
    });
    
    const response = await fetch('http://localhost:1234/api/products');
    const products = await response.json();
    
    expect(products).toHaveLength(1);
    expect(products[0]).toHaveProperty('id', 1);
  });
});

Deployment and DevOps#

Container-Based Deployment#

# Dockerfile for micro-frontend
FROM node:18-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# docker-compose.yml
version: '3.8'
services:
  shell:
    build: ./shell
    ports:
      - "3000:80"
    environment:
      - PRODUCTS_URL=http://products:80
      - CHECKOUT_URL=http://checkout:80
  
  products:
    build: ./products
    ports:
      - "3001:80"
  
  checkout:
    build: ./checkout
    ports:
      - "3002:80"
  
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - shell
      - products
      - checkout

Kubernetes Deployment#

# k8s/products-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: products-microfrontend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: products-microfrontend
  template:
    metadata:
      labels:
        app: products-microfrontend
    spec:
      containers:
      - name: products
        image: company/products-microfrontend:latest
        ports:
        - containerPort: 80
        env:
        - name: API_URL
          value: "https://api.company.com"
---
apiVersion: v1
kind: Service
metadata:
  name: products-service
spec:
  selector:
    app: products-microfrontend
  ports:
  - port: 80
    targetPort: 80
  type: ClusterIP

CI/CD Pipeline#

# .github/workflows/deploy.yml
name: Deploy Micro-frontend

on:
  push:
    branches: [main]
    paths: ['products/**']

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm ci
        working-directory: ./products
      - run: npm test
        working-directory: ./products
      - run: npm run test:integration
        working-directory: ./products

  build-and-deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Build Docker image
        run: |
          docker build -t company/products-microfrontend:${{ github.sha }} ./products
          docker tag company/products-microfrontend:${{ github.sha }} company/products-microfrontend:latest
      
      - name: Deploy to staging
        run: |
          kubectl set image deployment/products-microfrontend products=company/products-microfrontend:${{ github.sha }}
          kubectl rollout status deployment/products-microfrontend

Performance Optimization#

Bundle Optimization#

// Webpack optimization for micro-frontends
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
        shared: {
          name: 'shared',
          chunks: 'all',
          minChunks: 2,
        },
      },
    },
  },
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
  },
};

// Dynamic imports for lazy loading
const LazyProductList = React.lazy(() => 
  import('mfProducts/ProductList').catch(() => ({
    default: () => <div>Products unavailable</div>
  }))
);

Caching Strategies#

// Service worker for micro-frontend caching
const CACHE_NAME = 'mf-products-v1';
const REMOTE_CACHE = 'mf-remotes-v1';

self.addEventListener('fetch', event => {
  const { request } = event;
  
  // Cache micro-frontend bundles
  if (request.url.includes('remoteEntry.js')) {
    event.respondWith(
      caches.open(REMOTE_CACHE).then(cache => {
        return cache.match(request).then(response => {
          if (response) {
            // Serve from cache, update in background
            fetch(request).then(fetchResponse => {
              cache.put(request, fetchResponse.clone());
            });
            return response;
          }
          return fetch(request).then(fetchResponse => {
            cache.put(request, fetchResponse.clone());
            return fetchResponse;
          });
        });
      })
    );
  }
});

Common Pitfalls and Solutions#

1. Dependency Conflicts#

Problem: Different versions of shared libraries causing conflicts

Solution: Use Module Federation's shared dependencies

// webpack.config.js
new ModuleFederationPlugin({
  shared: {
    react: {
      singleton: true,
      requiredVersion: '^18.0.0',
    },
    'react-dom': {
      singleton: true,
      requiredVersion: '^18.0.0',
    },
  },
});

2. Performance Overhead#

Problem: Loading multiple bundles impacts performance

Solution: Implement smart loading strategies

// Preload critical micro-frontends
const preloadMicrofrontend = (name) => {
  const link = document.createElement('link');
  link.rel = 'preload';
  link.href = `${name}/remoteEntry.js`;
  link.as = 'script';
  document.head.appendChild(link);
};

// Preload on user interaction
button.addEventListener('mouseenter', () => {
  preloadMicrofrontend('products');
});

3. State Management Complexity#

Problem: Sharing state between micro-frontends becomes complex

Solution: Use event-driven architecture with clear boundaries

// Clear state boundaries
class MicrofrontendState {
  constructor(namespace) {
    this.namespace = namespace;
    this.state = new Map();
  }
  
  get(key) {
    return this.state.get(`${this.namespace}:${key}`);
  }
  
  set(key, value) {
    this.state.set(`${this.namespace}:${key}`, value);
    this.emit('stateChange', { key, value });
  }
  
  emit(event, data) {
    window.dispatchEvent(new CustomEvent(`${this.namespace}:${event}`, {
      detail: data
    }));
  }
}

Frequently Asked Questions#

Q: When should I choose micro-frontends over a monolith? A: Consider micro-frontends when you have multiple teams (10+ developers), need independent deployments, or are modernizing legacy systems. For smaller teams or simple applications, monoliths are often better.

Q: How do I handle SEO with micro-frontends? A: Use server-side composition or static site generation. Each micro-frontend should be able to render on the server and provide proper meta tags and structured data.

Q: What about bundle size and performance? A: Micro-frontends can increase bundle size due to duplication. Use shared dependencies, lazy loading, and proper caching strategies. Monitor performance metrics closely.

Q: How do I test micro-frontends? A: Use a combination of unit tests for individual micro-frontends, integration tests for communication, and contract tests for APIs. Consider using tools like Pact for contract testing.

Q: Can I mix different frameworks? A: Yes, that's one of the benefits of micro-frontends. You can use React for one part, Vue for another, and Angular for a third. However, this increases complexity and bundle size.

Micro-frontends are powerful but complex. They solve real problems for large organizations but introduce new challenges. Choose them when the benefits outweigh the costs, and implement them thoughtfully with proper tooling and processes.