Micro Frontend Design - Scalable UI Development with Independent Teams

2025.12.02

What is Micro Frontend

Micro frontend is an architecture pattern that applies the microservices concept to the frontend. It divides large applications into small, independently deployable applications.

Monolithic Frontend:

flowchart TB
    subgraph Mono["Single Application"]
        Product["Product Module"]
        Cart["Cart Module"]
        Account["Account Module"]
    end

Single deployment, single team

Micro Frontend:

flowchart TB
    subgraph Shell["Container App (Shell)"]
        Header["Shared Header"]
        subgraph Apps["Independent Apps"]
            Product2["Product App<br/>(Team A)"]
            Cart2["Cart App<br/>(Team B)"]
            Account2["Account App<br/>(Team C)"]
        end
    end

Independent deployment, independent teams, technology choice freedom

When to Consider Adoption

CaseReason
Large teams (10+ people)Ensure independence between teams
Gradual legacy system migrationCoexistence of old and new technologies
Different release cyclesIndependent deployment per feature
Domain-driven organizationTeam split by business domain
Technology stack diversityCoexistence of React, Vue, Angular

Integration Patterns

1. Module Federation (Webpack 5)

flowchart TB
    subgraph Host["Host App (Shell)"]
        Config["webpack.config.js<br/>remotes: products, cart, account"]
    end

    Host --> Products["Products Remote<br/>exposes: List, Detail"]
    Host --> Cart["Cart Remote<br/>exposes: Cart, Summary"]
    Host --> Account["Account Remote<br/>exposes: Profile, Settings"]
// host-app/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      remotes: {
        products: 'products@http://localhost:3001/remoteEntry.js',
        cart: 'cart@http://localhost:3002/remoteEntry.js',
        account: 'account@http://localhost:3003/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
};

// products-app/webpack.config.js
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'products',
      filename: 'remoteEntry.js',
      exposes: {
        './ProductList': './src/components/ProductList',
        './ProductDetail': './src/components/ProductDetail',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
};
// host-app/src/App.tsx
import React, { Suspense, lazy } from 'react';

// Dynamic import of remote components
const ProductList = lazy(() => import('products/ProductList'));
const Cart = lazy(() => import('cart/Cart'));
const AccountSettings = lazy(() => import('account/Settings'));

function App() {
  return (
    <div>
      <Header />
      <Routes>
        <Route
          path="/products/*"
          element={
            <Suspense fallback={<Skeleton />}>
              <ProductList />
            </Suspense>
          }
        />
        <Route
          path="/cart"
          element={
            <Suspense fallback={<Skeleton />}>
              <Cart />
            </Suspense>
          }
        />
        <Route
          path="/account/*"
          element={
            <Suspense fallback={<Skeleton />}>
              <AccountSettings />
            </Suspense>
          }
        />
      </Routes>
    </div>
  );
}

2. Single-SPA

// root-config.ts
import { registerApplication, start } from 'single-spa';

// Register micro frontends
registerApplication({
  name: '@myorg/products',
  app: () => System.import('@myorg/products'),
  activeWhen: ['/products'],
});

registerApplication({
  name: '@myorg/cart',
  app: () => System.import('@myorg/cart'),
  activeWhen: ['/cart'],
});

registerApplication({
  name: '@myorg/account',
  app: () => System.import('@myorg/account'),
  activeWhen: ['/account'],
});

// Shared component (always active)
registerApplication({
  name: '@myorg/navbar',
  app: () => System.import('@myorg/navbar'),
  activeWhen: () => true,
});

start();
// products-app/src/myorg-products.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import App from './App';

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

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

3. Native Federation (Vite/Rspack compatible)

// vite.config.ts
import { defineConfig } from 'vite';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    federation({
      name: 'host-app',
      remotes: {
        products: 'http://localhost:3001/assets/remoteEntry.js',
        cart: 'http://localhost:3002/assets/remoteEntry.js',
      },
      shared: ['react', 'react-dom'],
    }),
  ],
});

State Management and Communication

1. Custom Events (Recommended):

flowchart LR
    AppA["App A"] -->|CustomEvent| AppB["App B"]

2. Shared State (For complex cases):

flowchart TB
    AppA["App A"] --> Store["Store (Global)"]
    AppC["App C"] --> Store
    Store --> AppB["App B"]
    Store --> AppD["App D"]

3. URL/Query Params: Sharing navigation information

Custom Events

// Shared library
// @myorg/events
export const CartEvents = {
  ITEM_ADDED: 'cart:item-added',
  ITEM_REMOVED: 'cart:item-removed',
  CART_UPDATED: 'cart:updated',
} as const;

export interface CartItemEvent {
  productId: string;
  quantity: number;
  price: number;
}

export function dispatchCartEvent(
  type: string,
  detail: CartItemEvent
): void {
  window.dispatchEvent(new CustomEvent(type, { detail }));
}

export function subscribeToCartEvent(
  type: string,
  handler: (event: CustomEvent<CartItemEvent>) => void
): () => void {
  window.addEventListener(type, handler as EventListener);
  return () => window.removeEventListener(type, handler as EventListener);
}
// products-app/src/ProductCard.tsx
import { CartEvents, dispatchCartEvent } from '@myorg/events';

function ProductCard({ product }) {
  const handleAddToCart = () => {
    dispatchCartEvent(CartEvents.ITEM_ADDED, {
      productId: product.id,
      quantity: 1,
      price: product.price,
    });
  };

  return (
    <div>
      <h3>{product.name}</h3>
      <button onClick={handleAddToCart}>Add to Cart</button>
    </div>
  );
}

// cart-app/src/Cart.tsx
import { useEffect, useState } from 'react';
import { CartEvents, subscribeToCartEvent } from '@myorg/events';

function Cart() {
  const [items, setItems] = useState([]);

  useEffect(() => {
    const unsubscribe = subscribeToCartEvent(CartEvents.ITEM_ADDED, (event) => {
      setItems((prev) => [...prev, event.detail]);
    });

    return unsubscribe;
  }, []);

  return (
    <div>
      <h2>Cart ({items.length} items)</h2>
      {/* ... */}
    </div>
  );
}

Global State Sharing

// @myorg/shared-store
import { create } from 'zustand';

interface User {
  id: string;
  name: string;
  email: string;
}

interface AuthState {
  user: User | null;
  isAuthenticated: boolean;
  login: (user: User) => void;
  logout: () => void;
}

// Globally shared store
export const useAuthStore = create<AuthState>((set) => ({
  user: null,
  isAuthenticated: false,
  login: (user) => set({ user, isAuthenticated: true }),
  logout: () => set({ user: null, isAuthenticated: false }),
}));

// Usage in each micro frontend
// header-app/src/Header.tsx
import { useAuthStore } from '@myorg/shared-store';

function Header() {
  const { user, isAuthenticated, logout } = useAuthStore();

  return (
    <header>
      {isAuthenticated ? (
        <>
          <span>Welcome, {user?.name}</span>
          <button onClick={logout}>Logout</button>
        </>
      ) : (
        <a href="/login">Login</a>
      )}
    </header>
  );
}

Routing Strategy

// shell-app/src/routing.ts
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { lazy, Suspense } from 'react';

// Dynamic loading of each micro frontend's routes
const ProductsRoutes = lazy(() => import('products/Routes'));
const CartRoutes = lazy(() => import('cart/Routes'));
const AccountRoutes = lazy(() => import('account/Routes'));

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      { index: true, element: <Home /> },
      {
        path: 'products/*',
        element: (
          <Suspense fallback={<Loading />}>
            <ProductsRoutes />
          </Suspense>
        ),
      },
      {
        path: 'cart/*',
        element: (
          <Suspense fallback={<Loading />}>
            <CartRoutes />
          </Suspense>
        ),
      },
      {
        path: 'account/*',
        element: (
          <Suspense fallback={<Loading />}>
            <AccountRoutes />
          </Suspense>
        ),
      },
    ],
  },
]);

export function AppRouter() {
  return <RouterProvider router={router} />;
}

Deployment Strategy

CDN Structure:

PathVersion
shell/v1.2.3/
products/v2.1.0/
cart/v1.8.5/
account/v3.0.1/

Manifest Management:

{
  "products": "https://cdn/products/v2.1.0/",
  "cart": "https://cdn/cart/v1.8.5/",
  "account": "https://cdn/account/v3.0.1/"
}

Blue-Green / Canary deployment compatible

Shared UI Components

// @myorg/ui-kit (Shared design system)
export { Button } from './Button';
export { Input } from './Input';
export { Card } from './Card';
export { Modal } from './Modal';
export { theme } from './theme';

// Usage in each micro frontend
// webpack.config.js
{
  shared: {
    '@myorg/ui-kit': {
      singleton: true,
      requiredVersion: '^1.0.0',
    },
  },
}

Testing Strategy

LevelTargetTools
UnitIndividual componentsVitest, Jest
IntegrationSingle micro FETesting Library
E2EFull integrationPlaywright, Cypress
ContractAPI contractsPact
← Back to list