Cloud and platform

Micro-frontends: Architecture, Patterns, and Production Decisions in 2026

Single monolithic frontends don't scale with multiple independent teams. Micro-frontends enable autonomous teams to develop, deploy, and evolve interfaces without centralized coordination.

3/16/20269 min readCloud
Micro-frontends: Architecture, Patterns, and Production Decisions in 2026

Executive summary

Single monolithic frontends don't scale with multiple independent teams. Micro-frontends enable autonomous teams to develop, deploy, and evolve interfaces without centralized coordination.

Last updated: 3/16/2026

Executive summary

The traditional monolithic frontend—a single application codebase served by a centralized team—works well until it doesn't. As multiple independent teams start contributing to the same interface, merge conflicts, blocked deployments, and coupled dependencies make development progressively slower.

Micro-frontends decouple domain interfaces from code boundaries. Each team maintains its own autonomous frontend, deployed independently, and composed at runtime into a unified experience for the user.

In 2026, micro-frontends have evolved from iframe composition experiments to mature patterns based on module federation, intelligent routing, and component composition. Organizations that adopt micro-frontends correctly reduce team conflicts, accelerate time-to-market, and enable multiple teams to work in parallel without manual coordination.

The monolithic frontend problem

Symptoms of single SPA that doesn't scale

1. Blocked deployments

A trivial change in one feature blocks the deployment of the entire application because all teams share the same codebase and deployment cycle.

2. Merge conflicts

Multiple teams modifying the same App.tsx, routes.tsx, or shared component files create frequent merge conflicts that require manual resolution.

3. Shared tech debt

When one team makes a suboptimal technical decision (e.g., obsolete library), all teams inherit that technical debt because they share the same bundle.

4. Inability to evolve independently

Team A cannot modernize their stack (React 18 → 19) without coordinating with Teams B, C, and D, all of which use the same build.

5. Scaled build times

As the monolithic frontend grows, build times scale from minutes to hours, reducing developer productivity.

When monolithic makes sense

Don't introduce unnecessary complexity. Monolithic frontends still make sense when:

  • You have a single frontend team (< 10 developers)
  • There are no plans to scale to multiple teams
  • Your application has low business complexity
  • Performance and simplicity are priorities over team flexibility

Micro-frontend composition patterns

Pattern 1: Module Federation (Webpack 5)

Module Federation allows multiple applications to share code at runtime without build coordination.

typescript// app1/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
      filename: 'remoteEntry.js',
      exposes: {
        './ProductList': './src/components/ProductList',
        './Checkout': './src/components/Checkout',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
};

// app2/webpack.config.js
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'app2',
      remotes: {
        app1: 'app1@http://localhost:3001/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
};

// Consuming remote component
import React from 'react';
const ProductList = React.lazy(() => import('app1/ProductList'));

function App() {
  return (
    <div>
      <h1>App 2</h1>
      <React.Suspense fallback={<div>Loading...</div>}>
        <ProductList />
      </React.Suspense>
    </div>
  );
}

Advantages:

  • Native code sharing (React, libraries)
  • Hot reloading in development
  • Independent versioning
  • Full TypeScript support

Trade-offs:

  • Webpack configuration complexity
  • More complex runtime debugging
  • Need to manage shared dependency versions

Pattern 2: Single-SPA with Routing

A shell application that routes to different micro-frontends based on URL.

typescript// shell/src/App.tsx
import React, { useState, useEffect } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

interface MicroFrontend {
  route: string;
  url: string;
  module: string;
}

const microFrontends: MicroFrontend[] = [
  {
    route: '/dashboard',
    url: 'http://localhost:3001/remoteEntry.js',
    module: './Dashboard',
  },
  {
    route: '/checkout',
    url: 'http://localhost:3002/remoteEntry.js',
    module: './Checkout',
  },
  {
    route: '/products',
    url: 'http://localhost:3003/remoteEntry.js',
    module: './ProductList',
  },
];

function Shell() {
  const [activeRoute, setActiveRoute] = useState<string | null>(null);

  useEffect(() => {
    const path = window.location.pathname;
    const mf = microFrontends.find(mf => path.startsWith(mf.route));

    if (mf && activeRoute !== mf.module) {
      setActiveRoute(mf.module);

      // Dynamically load micro-frontend
      const script = document.createElement('script');
      script.src = mf.url;
      script.async = true;
      document.body.appendChild(script);
    }
  }, [window.location.pathname]);

  return (
    <BrowserRouter>
      <Routes>
        {microFrontends.map(mf => (
          <Route
            key={mf.route}
            path={`${mf.route}/*`}
            element={<div id={`${mf.module}-mount`} />}
          />
        ))}
        <Route path="*" element={<div>404</div>} />
      </Routes>
    </BrowserRouter>
  );
}

Advantages:

  • Simple and familiar URL routing
  • Each micro-frontend can have its own internal router
  • Easy for debugging (each MF has its own dev URL)

Trade-offs:

  • Requires maintained shell application
  • Coupling between shell and micro-frontends via URL
  • Difficulty sharing state between MFs

Pattern 3: Composition via API (BFF)

Frontend fetches components from a composition API.

typescript// composicao-bff/api/compose.ts
import { NextResponse } from 'next/server';

interface ComponentRequest {
  id: string;
  props: Record<string, any>;
}

export async function POST(req: Request) {
  const requests: ComponentRequest[] = await req.json();

  // Fetch each component in parallel
  const components = await Promise.all(
    requests.map(async (request) => {
      const response = await fetch(
        `http://${request.id}-service:3000/component/${request.id}`,
        {
          method: 'POST',
          body: JSON.stringify(request.props),
        }
      );
      return { id: request.id, html: await response.text() };
    })
  );

  return NextResponse.json({ components });
}

// shell/src/Page.tsx
import { useEffect, useState } from 'react';

interface ComposedPageProps {
  layout: string[];
  components: string[];
}

function ComposedPage({ layout, components }: ComposedPageProps) {
  const [composedHTML, setComposedHTML] = useState<Record<string, string>>({});

  useEffect(() => {
    // Fetch HTML for each component
    fetch('/api/compose', {
      method: 'POST',
      body: JSON.stringify(components),
    }).then(res => res.json())
      .then(data => {
        setComposedHTML(data.components);
      });
  }, [components]);

  return (
    <div>
      {layout.map((slot, i) => (
        <div key={i} className={`slot-${slot}`}>
          <div
            dangerouslySetInnerHTML={{ __html: composedHTML[slot] || '' }}
          />
        </div>
      ))}
    </div>
  );
}

Advantages:

  • Micro-frontends can be server-rendered
  • Facilitates SEO (each MF can control meta tags)
  • Technology flexibility (different stacks per MF)

Trade-offs:

  • HTTP request overhead
  • No shared JavaScript (each MF loads its own bundle)
  • Composition complexity

Pattern 4: Web Components

Browser-native pattern for component composition.

typescript// checkout-service/src/CheckoutElement.ts
import { customElements } from 'html-element';

class CheckoutElement extends HTMLElement {
  connectedCallback() {
    this.render();
  }

  render() {
    this.innerHTML = `
      <div class="checkout-container">
        <h2>Checkout</h2>
        <form id="checkout-form">
          <!-- Checkout form -->
        </form>
      </div>
    `;

    this.addEventListener('submit', this.handleSubmit.bind(this));
  }

  handleSubmit(event: Event) {
    event.preventDefault();

    // Dispatch custom event to shell
    this.dispatchEvent(
      new CustomEvent('checkout-submit', {
        detail: { formData: this.getFormData() },
        bubbles: true
      })
    );
  }

  getFormData(): Record<string, string> {
    const form = this.querySelector('#checkout-form');
    const formData = new FormData(form as HTMLFormElement);
    return Object.fromEntries(formData.entries()) as Record<string, string>;
  }
}

customElements.define('checkout-element', CheckoutElement);

// shell/src/App.tsx
function Shell() {
  return (
    <div>
      <h1>E-commerce Platform</h1>
      <checkout-element />
    </div>
  );
}

Advantages:

  • Browser-native pattern (framework-agnostic)
  • Style sharing via Shadow DOM
  • Facilitates MF integration with different technologies

Trade-offs:

  • Shadow DOM limits external styling
  • Less ergonomic for developers accustomed to React
  • More difficult debugging

Shared state management

Pattern 1: Event Bus (Custom Events)

typescript// shared/src/EventBus.ts
class EventBus {
  private listeners: Map<string, Set<Function>> = new Map();

  on(event: string, callback: Function): () => void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }

    this.listeners.get(event)!.add(callback);

    // Return unsubscribe function
    return () => this.listeners.get(event)!.delete(callback);
  }

  emit(event: string, data: any): void {
    const callbacks = this.listeners.get(event);

    if (callbacks) {
      callbacks.forEach(callback => callback(data));
    }
  }
}

export const eventBus = new EventBus();

// checkout-mf/src/components/Checkout.tsx
import { eventBus } from '@shared/event-bus';

function Checkout() {
  const handleCheckoutComplete = (data: any) => {
    console.log('Checkout completed:', data);

    // Notify other MFs
    eventBus.emit('cart:cleared', {});
  };

  useEffect(() => {
    const unsubscribe = eventBus.on('checkout:complete', handleCheckoutComplete);

    return unsubscribe;
  }, []);

  return <div>Checkout</div>;
}

// cart-mf/src/components/Cart.tsx
function Cart() {
  const [cartItems, setCartItems] = useState([]);

  useEffect(() => {
    // Listen for checkout cleared event
    const unsubscribe = eventBus.on('cart:cleared', () => {
      setCartItems([]);
    });

    return unsubscribe;
  }, []);

  return <div>Cart</div>;
}

Pattern 2: Shared Context (via Module Federation)

typescript// shared/src/AppContext.tsx
import React, { createContext, useContext } from 'react';

interface AppContextType {
  user: User | null;
  cart: CartItem[];
  setUser: (user: User) => void;
  addToCart: (item: CartItem) => void;
}

const AppContext = createContext<AppContextType | undefined>(undefined);

export function AppProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [cart, setCart] = useState<CartItem[]>([]);

  const addToCart = (item: CartItem) => {
    setCart(prev => [...prev, item]);
  };

  return (
    <AppContext.Provider value={{ user, cart, setUser, addToCart }}>
      {children}
    </AppContext.Provider>
  );
}

export function useAppContext() {
  const context = useContext(AppContext);

  if (!context) {
    throw new Error('useAppContext must be used within AppProvider');
  }

  return context;
}

// checkout-mf/src/components/Checkout.tsx
import { useAppContext } from '@shared/AppContext';

function Checkout() {
  const { cart, user } = useAppContext();

  function handleCheckout() {
    if (!user) {
      // User not logged in - redirect to auth MF
      window.location.href = '/auth';
      return;
    }

    // Process checkout with shared cart items
    processCheckout(cart);
  }

  return <div>Checkout</div>;
}

Code and asset sharing

Pattern 1: Shared Module via Module Federation

typescript// shared/webpack.config.js
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'shared',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/components/Button',
        './Input': './src/components/Input',
        './api': './src/api',
        './types': './src/types',
        './utils': './src/utils',
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
      },
    }),
  ],
};

// product-mf/webpack.config.js
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'product',
      remotes: {
        shared: 'shared@http://localhost:3000/remoteEntry.js',
      },
    }),
  ],
};

// product-mf/src/components/Product.tsx
import { Button } from 'shared/Button';
import { ProductAPI } from 'shared/api';

function Product({ id }: { id: string }) {
  const [product, setProduct] = useState(null);

  useEffect(() => {
    ProductAPI.getProduct(id).then(setProduct);
  }, [id]);

  if (!product) return <div>Loading...</div>;

  return (
    <div>
      <h2>{product.name}</h2>
      <p>{product.price}</p>
      <Button onClick={() => addToCart(product)}>
        Add to Cart
      </Button>
    </div>
  );
}

Pattern 2: Monorepo with Shared Packages

typescript// monorepo/package.json
{
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build"
  }
}

// packages/shared/package.json
{
  "name": "@acme/shared",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "exports": {
    ".": "./dist/index.js",
    "./Button": "./dist/components/Button.js",
    "./api": "./dist/api/index.js",
    "./types": "./dist/types/index.js"
  }
}

// packages/product-mf/package.json
{
  "name": "@acme/product-mf",
  "dependencies": {
    "@acme/shared": "workspace:*",
    "react": "^18.0.0"
  }
}

// product-mf/src/components/Product.tsx
import { Button } from '@acme/shared/Button';
import { ProductAPI } from '@acme/shared/api';

Routing and navigation

Pattern 1: Cross-MF Routing via Event Bus

typescript// shared/src/RouterService.ts
class RouterService {
  navigate(route: string, params?: Record<string, any>): void {
    const event = new CustomEvent('navigate', {
      detail: { route, params },
      bubbles: true
    });
    window.dispatchEvent(event);
  }
}

export const router = new RouterService();

// shell/src/App.tsx
import { router } from '@shared/RouterService';

function Shell() {
  useEffect(() => {
    // Listen for navigation events
    const handleNavigate = (event: Event) => {
      const { route, params } = (event as CustomEvent).detail;
      window.history.pushState({}, '', route);
      // Update active MF based on route
      updateActiveMF(route);
    };

    window.addEventListener('navigate', handleNavigate);

    return () => window.removeEventListener('navigate', handleNavigate);
  }, []);

  return <div>Shell content</div>;
}

// product-mf/src/components/Product.tsx
function Product({ id }: { id: string }) {
  const handleViewCart = () => {
    // Navigate to cart MF
    router.navigate('/cart', { productId: id });
  };

  return (
    <div>
      <h2>Product</h2>
      <button onClick={handleViewCart}>View Cart</button>
    </div>
  );
}

Pattern 2: Single Router via Shell

typescript// shell/src/App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';

const routes = [
  { path: '/products/*', mf: 'product-mf' },
  { path: '/cart/*', mf: 'cart-mf' },
  { path: '/checkout/*', mf: 'checkout-mf' },
  { path: '/account/*', mf: 'account-mf' },
];

function Shell() {
  const [activeMF, setActiveMF] = useState<string | null>(null);

  useEffect(() => {
    const path = window.location.pathname;
    const route = routes.find(r => path.startsWith(r.path));

    if (route && activeMF !== route.mf) {
      setActiveMF(route.mf);

      // Load script for corresponding MF
      const script = document.createElement('script');
      script.src = `http://localhost:3000/${route.mf}/remoteEntry.js`;
      document.body.appendChild(script);
    }
  }, [window.location.pathname]);

  return (
    <BrowserRouter>
      <Routes>
        {routes.map(route => (
          <Route
            key={route.path}
            path={route.path}
            element={<div id={`${route.mf}-container`} />}
          />
        ))}
      </Routes>
    </BrowserRouter>
  );
}

Shared styles and CSS

Pattern 1: Design Tokens via JavaScript

typescript// shared/src/tokens.ts
export const tokens = {
  colors: {
    primary: '#0066cc',
    secondary: '#6c757d',
    success: '#28a745',
    danger: '#dc3545',
  },
  spacing: {
    xs: '4px',
    sm: '8px',
    md: '16px',
    lg: '24px',
    xl: '32px',
  },
  typography: {
    fontSize: {
      xs: '12px',
      sm: '14px',
      base: '16px',
      lg: '18px',
      xl: '20px',
    },
    fontWeight: {
      normal: 400,
      medium: 500,
      bold: 600,
    },
  },
  borderRadius: {
    sm: '4px',
    md: '8px',
    lg: '12px',
  },
};

// product-mf/src/components/Button.tsx
import { tokens } from '@shared/tokens';

const buttonStyles = {
  base: `
    padding: ${tokens.spacing.sm} ${tokens.spacing.lg};
    font-size: ${tokens.typography.fontSize.base};
    font-weight: ${tokens.typography.fontWeight.medium};
    border-radius: ${tokens.borderRadius.md};
    border: none;
    cursor: pointer;
    transition: all 0.2s ease;
  `,
  primary: `
    background-color: ${tokens.colors.primary};
    color: white;
  `,
  secondary: `
    background-color: ${tokens.colors.secondary};
    color: white;
  `,
};

function Button({ variant = 'primary', children }: ButtonProps) {
  return (
    <button
      className={`${buttonStyles.base} ${buttonStyles[variant]}`}
    >
      {children}
    </button>
  );
}

Pattern 2: CSS Modules Scoped

css/* product-mf/src/components/Product.module.css */
.productContainer {
  padding: 16px;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
}

.productName {
  font-size: 18px;
  font-weight: 600;
  margin-bottom: 8px;
}

.productPrice {
  color: #0066cc;
  font-size: 16px;
  font-weight: 500;
}

/* Prefix to avoid conflicts */
:global(.shared-button) {
  background-color: var(--primary-color);
  color: white;
}

Common anti-patterns

Anti-pattern 1: Iframes as only strategy

Problem: Iframes completely isolate MFs, making communication, shared styling, and accessibility difficult.

Consequences:

  • Double scroll (iframe + window)
  • Difficulty sharing state
  • Poor SEO (crawlers don't index content inside iframes)
  • Reduced performance (iframe overhead)

Solution: Use Module Federation, Web Components, or composition via API. Reserve iframes only for third-party MFs you don't control.

Anti-pattern 2: Versioning without compatibility

Problem: Updating a shared dependency (React) without coordinating with all MFs.

Consequences:

  • Unexpected runtime errors
  • Multiple React versions loaded simultaneously
  • Undefined behavior

Solution: Use singleton: true in Module Federation and implement compatibility strategies (semver ranges, version adapters).

Anti-pattern 3: Strong coupling via direct DOM access

Problem: MFs access other MFs' DOM directly via document.getElementById.

Consequences:

  • Fragility (DOM changes break integration)
  • Violation of encapsulation
  • Difficult debugging

Solution: Use Event Bus, Context, or well-defined APIs for cross-MF communication.

Implementation decision

Questions to decide for micro-frontends

  1. Do you have multiple independent teams working on frontend?
  • Yes → Micro-frontends can reduce conflicts
  • No → Complexity may not be justified
  1. Do teams need to deploy independently?
  • Yes → Micro-frontends with decoupled deployments
  • No → Single SPA with unified CI/CD may be sufficient
  1. Do different teams need different technical stacks?
  • Yes → Composition via API or Web Components
  • No → Module Federation allows easy React sharing
  1. Is SEO critical for your application?
  • Yes → Prefer SSR-friendly patterns (API composition, Next.js App Router)
  • No → Client-side composition is acceptable

Pattern selection matrix

RequirementModule FederationAPI CompositionWeb Components
Code sharingExcellentPoorPoor
Independent deploymentsExcellentExcellentExcellent
SSR-friendlyLimitedExcellentLimited
Framework-agnosticPoorExcellentExcellent
Dev experienceGoodFairFair
Runtime overheadLowHighLow
Initial complexityHighMediumMedium

Implementation plan in 90 days

Phase 1: Proof of Concept (30 days)

  • Identify 2-3 business domains for decomposition
  • Implement POC with Module Federation
  • Define shared packages (tokens, components, types)
  • Validate development and deployment workflow

Phase 2: Pilot with Team (30 days)

  • Migrate one complete domain to micro-frontend
  • Implement routing and navigation patterns
  • Establish review and deployment processes
  • Collect developer feedback

Phase 3: Expansion (30 days)

  • Migrate additional domains
  • Implement cross-MF performance monitoring
  • Document patterns and conventions
  • Train additional teams

Monitoring and observability

Critical metrics for micro-frontends

1. Loading performance

  • TTFB (Time to First Byte) per MF
  • FCP (First Contentful Paint) per MF
  • TTI (Time to Interactive) per MF

2. Runtime metrics

  • MF loading errors
  • Cross-MF communication latency
  • Number of re-renders per MF

3. Business metrics

  • Error rates per MF
  • Conversion per MF
  • Time to checkout/critical action

Monitoring implementation

typescript// shared/src/MFMonitor.ts
class MFMonitor {
  private metrics: Map<string, any> = new Map();

  trackMFTiming(mfName: string, metric: string, value: number): void {
    const key = `${mfName}:${metric}`;
    this.metrics.set(key, value);

    // Send to monitoring platform
    this.sendToMonitoring('mf_timing', { mf: mfName, metric, value });
  }

  trackMFError(mfName: string, error: Error): void {
    this.sendToMonitoring('mf_error', {
      mf: mfName,
      error: error.message,
      stack: error.stack,
      timestamp: new Date().toISOString()
    });
  }

  private sendToMonitoring(event: string, data: any): void {
    // Implementation depends on monitoring platform (DataDog, New Relic, etc.)
    if (typeof window !== 'undefined' && (window as any).analytics) {
      (window as any).analytics.track(event, data);
    }
  }
}

export const mfMonitor = new MFMonitor();

// product-mf/src/index.tsx
import { mfMonitor } from '@shared/MFMonitor';

try {
  const startLoad = performance.now();

  // Load component
  const component = await loadProductComponent(id);

  const loadTime = performance.now() - startLoad;
  mfMonitor.trackMFTiming('product-mf', 'load_time', loadTime);
} catch (error) {
  mfMonitor.trackMFError('product-mf', error as Error);
}

Conclusion

Micro-frontends in 2026 are a mature architecture for frontend teams that need to scale development with multiple independent teams. Module Federation, API composition, and Web Components offer different trade-offs in code sharing, performance, and operational complexity.

The decision to implement micro-frontends should not be based on technical hype—it should be based on real problems: team conflicts, blocked deployments, and inability to evolve independently. When implemented correctly, micro-frontends decouple teams, reduce time-to-market, and enable multiple technical stacks to coexist in the same unified interface.

Closing practical question: Can your current frontend be developed, tested, and deployed by multiple independent teams without conflicts or blocks?


Need to design or implement micro-frontend architecture to scale your frontend development? Talk to Imperialis about micro-frontend architecture, pattern selection, and production implementation.

Sources

Related reading