Micro-Frontends: Splitting a Monolith UI Without the Pain

Your frontend monolith has 200 components, 15 teams, and deploys take 45 minutes. Micro-frontends let teams ship independently. Learn Module Federation, shared dependencies, routing strategies, and when NOT to use them.

Micro-Frontends: Splitting a Monolith UI Without the Pain illustration
On this page12 sections

Your frontend started as a small React or Angular app. Now it has 200+ components, multiple teams working on different features, and a single deployment pipeline that bottlenecks everyone. Micro-frontends solve this by splitting the UI into independently deployable pieces, each owned by a different team. Before splitting, compare that organizational boundary with a monorepo or polyrepo codebase structure and the built-in conventions of Angular 21 for large applications.

But micro-frontends add real complexity. This guide covers the architecture patterns, Module Federation implementation, and the honest tradeoffs so you can decide if the complexity is worth it for your team.

What Are Micro-Frontends?

Micro-frontends apply microservice principles to the frontend: each feature area is an independent application that can be developed, tested, and deployed separately.

  • Shell/Host: The container app that provides navigation, authentication, and shared layout
  • Remotes/Micro-apps: Independent applications loaded into the shell at runtime
  • Shared dependencies: Libraries loaded once and shared across micro-apps (React, Angular, design system)

Integration Approaches

Approach How It Works Pros Cons
Module Federation Webpack/Vite loads remote bundles at runtime Shared deps, typed contracts, HMR Build tool lock-in
iframes Each micro-app in an iframe Complete isolation Poor UX, no shared state
Web Components Custom elements wrapping each app Framework agnostic Shadow DOM quirks, SSR challenges
Build-time integration NPM packages composed at build Simple, typed Not independently deployable

Module Federation: The Modern Standard

Webpack 5 Module Federation lets multiple independent builds share code at runtime. One application can dynamically load modules from another deployed application.

Shell Application (Host)

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

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      remotes: {
        // Load micro-apps from their deployed URLs
        dashboard: 'dashboard@https://dashboard.example.com/remoteEntry.js',
        settings: 'settings@https://settings.example.com/remoteEntry.js',
        billing: 'billing@https://billing.example.com/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
        '@company/design-system': { singleton: true },
      },
    }),
  ],
};

Remote Application (Micro-App)

// webpack.config.js (Dashboard micro-app)
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'dashboard',
      filename: 'remoteEntry.js',     // Entry point for the shell to load
      exposes: {
        './DashboardApp': './src/DashboardApp',   // Exposed component
        './DashboardWidget': './src/widgets/Summary',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
        '@company/design-system': { singleton: true },
      },
    }),
  ],
};

Loading Remote Components

// Shell routing with lazy-loaded micro-apps
import { lazy, Suspense } from 'react';

// Dynamic imports from remote applications
const DashboardApp = lazy(() => import('dashboard/DashboardApp'));
const SettingsApp = lazy(() => import('settings/SettingsApp'));
const BillingApp = lazy(() => import('billing/BillingApp'));

function App() {
  return (
    <Shell>
      <Navigation />
      <Suspense fallback={<LoadingSpinner />}>
        <Routes>
          <Route path="/dashboard/*" element={<DashboardApp />} />
          <Route path="/settings/*" element={<SettingsApp />} />
          <Route path="/billing/*" element={<BillingApp />} />
        </Routes>
      </Suspense>
    </Shell>
  );
}

Shared State and Communication

// Option 1: Custom Events (loosely coupled)
// Micro-app dispatches:
window.dispatchEvent(new CustomEvent('user:updated', {
  detail: { userId: 123, name: 'Alice' }
}));

// Shell or another micro-app listens:
window.addEventListener('user:updated', (event) => {
  updateUserContext(event.detail);
});

// Option 2: Shared state store (via Module Federation shared module)
// shared-store.ts (exposed by shell, consumed by remotes)
import { create } from 'zustand';

export const useAppStore = create((set) => ({
  user: null,
  theme: 'light',
  setUser: (user) => set({ user }),
  setTheme: (theme) => set({ theme }),
}));

// Any micro-app can import and use:
import { useAppStore } from 'shell/SharedStore';
const user = useAppStore((state) => state.user);

Shared Design System

// Publish your design system as a shared singleton
// All micro-apps use the SAME instance loaded once

// webpack.config.js (every app)
shared: {
  '@company/design-system': {
    singleton: true,         // Only load ONE instance
    eager: false,            // Lazy load
    requiredVersion: '^3.0.0',
  },
}

// This ensures:
// 1. Consistent look across all micro-apps
// 2. One CSS bundle for the design system (not duplicated)
// 3. Version compatibility enforcement

Independent Deployment Pipeline

# .github/workflows/deploy-dashboard.yml
name: Deploy Dashboard Micro-App

on:
  push:
    paths:
      - 'apps/dashboard/**'     # Only trigger for dashboard changes

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build
      - run: npm test

      # Deploy to CDN independently
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: us-east-1
      - run: |
          aws s3 sync dist/ s3://micro-frontends/dashboard/ --delete
          aws cloudfront create-invalidation --distribution-id DIST_ID --paths "/dashboard/*"

# Each micro-app has its own pipeline
# Dashboard team deploys without waiting for billing team
# Shell only redeploys when shell code changes

When NOT to Use Micro-Frontends

  • Small team (less than 3-4 frontend developers): The coordination overhead exceeds the benefit
  • Simple application: If one team can manage the entire frontend, a monolith is simpler
  • Strong coupling between features: If features share lots of state and UI, splitting them creates more problems
  • No independent deployment need: If you deploy everything together anyway, micro-frontends add complexity for no gain
  • Performance-critical apps: Multiple bundles, runtime loading, and shared dependency negotiation add latency

Challenges and Mitigations

Challenge Mitigation
Inconsistent UI Shared design system as singleton dependency
Shared state complexity Custom events for loose coupling, shared store for tight coupling
Version conflicts Singleton shared dependencies with version ranges
Performance overhead Preload critical remotes, share common chunks
Local development Run shell + one remote locally, mock others
Testing across boundaries Contract tests + integration tests in staging

Key Takeaways

  • Micro-frontends solve organizational problems, not technical ones - use them when multiple teams need to deploy independently
  • Module Federation is the current standard - runtime integration with shared dependencies
  • Shared singletons prevent bundle duplication - React, design system, and state libraries should load once
  • Communication via custom events keeps coupling low - micro-apps should not import from each other directly
  • Each micro-app gets its own CI/CD pipeline - that is the whole point
  • Do not split too early - start with a well-structured monolith and split only when team autonomy demands it
  • The complexity cost is real - only worth it with 4+ teams working on the same frontend

Micro-frontends are a scaling strategy for organizations, not a technical improvement for applications. If you have one team, a monolith with good module boundaries is strictly better. If you have five teams stepping on each other during deployments, micro-frontends give each team their own lane. Match the architecture to the organization, not the other way around.

Share this article

Stuck on implementation?

Get private, 1-on-1 help with system design, performance, scaling, or any technical challenge.

Book a Session

Related Production Resources

Course

Free learning tracks

Turn this guide into a structured production engineering path.

Lab

Interactive engineering labs

Practice the same ideas through scenario-based simulators.

Reference

Production cheatsheets

Keep the operational commands and checks nearby.

Glossary

Key terms

Review the vocabulary behind the architecture.

Discussion

Questions, corrections, or production notes? Add them here so other learners can benefit.

Comments load on demand

To keep this article fast and private by default, the GitHub-powered discussion loads only when you reach this section.

Prefer GitHub? Open the project discussions directly .

Continue Reading

Related practical guides from the same production engineering path.

Frontend 12 min read

Web Authentication API: Passwordless Login with Passkeys

Passwords are the weakest link in security. Passkeys replace them with biometrics and device-bound credentials. Learn WebAuthn, FIDO2, and how to implement passwordless login in your web app with real code examples.

WebAuthn Passkeys
Frontend 11 min read

CSS Grid and Flexbox Mastery: Stop Guessing, Start Designing

Flexbox is for one-dimensional flow. Grid is for two-dimensional layout. Learn when to use which, build responsive layouts without media queries, and master the patterns that replace 90% of CSS frameworks.

CSS Flexbox
Frontend 28 min read

Building Your Own Design System: The Complete Practical Guide

A no-nonsense, production-grade guide to building a design system from scratch. Covers design tokens, component architecture, documentation, versioning, and the mistakes that kill most design systems before they launch.

Design System Frontend
Frontend 12 min read

Web Performance Secrets: Core Web Vitals from Red to Green

Your Lighthouse score is red. Users are bouncing. Google is penalizing your rankings. This guide fixes LCP, CLS, and INP with practical techniques - lazy loading, image optimization, font strategies, and real before-after audits.

Web Performance Core Web Vitals