5 MFE Integration Patterns

5 Micro Frontend Integration Patterns You Should Know

There is no single way to integrate Micro Frontends. The approach you choose directly impacts performance, developer experience, shared state capabilities, and deployment independence. Choosing the wrong pattern early can force a costly rewrite later.

In this article, you will learn the five main Micro Frontend integration patterns, see real code examples for each one, understand the trade-offs, and learn why Module Federation with shared npm packages is the most widely adopted approach for production applications.

New to Micro Frontend? Start with What is Micro Frontend Architecture? first. Already decided on MFE? Read Micro Frontend vs Monolith for the decision framework.

Overview: 5 Integration Patterns

5 Micro Frontend integration patterns comparison diagram showing Build-Time, Server-Side, iframes, Runtime JavaScript, and Module Federation with pros and cons for each

PatternHow It WorksShared StateIndependent DeployPerformance
Build-Time (npm packages)Shared code as workspace packagesYes (same bundle)No (rebuild needed)Excellent
Server-Side (SSR/ISR)Server composes HTML before sendingLimitedYesExcellent
iframesEach MFE in a separate iframeNoYesPoor
Runtime JavaScriptLazy-load remote bundles at runtimeVia shared storeYesGood
Module FederationWebpack shares modules at runtimeYes (singleton)YesExcellent

Most production MFE applications use Pattern 1 + Pattern 5 together — shared npm workspace packages for common code (store, API, SEO) and Module Federation for runtime integration of independent MFE applications.

Pattern 1: Build-Time Integration (npm Workspace Packages)

Shared code — Redux store, API client, SEO utilities — is packaged as npm workspace packages that all MFEs import at build time. This is not technically "runtime integration" but it is a critical piece of every MFE architecture.

How It Works

  1. Create shared packages in a packages/ directory
  2. Configure npm workspaces (opens in a new tab) or Turborepo (opens in a new tab) in the root package.json
  3. Every MFE imports from the shared package name (e.g., @myapp/store)
  4. When you run npm install, workspaces symlink the packages automatically

Code Example

package.json
// Root package.json — npm workspaces configuration
{
  "name": "my-ecommerce-app",
  "workspaces": [
    "apps/*",
    "packages/*",
    "packages/*/*"
  ],
  "scripts": {
    "dev": "turbo run dev --concurrency=15",
    "build": "turbo run build",
    "start": "turbo run start"
  }
}

What Gets Shared as Build-Time Packages

PackagePurposeUsed By
@myapp/storeRedux store, slices, typed hooksAll MFEs
@myapp/apiAxios instance with auth interceptorsAll MFEs
@myapp/seoOptimized image components, JSON-LD schemasNext.js MFEs

Build-Time packages + Module Federation work together. The shared packages are bundled into each MFE at build time, but Module Federation's singleton: true ensures only one instance is loaded at runtime. So @myapp/store is in every MFE's bundle, but at runtime, all MFEs share the same store instance.

When to Use

Use WhenAvoid When
Shared utilities needed by all MFEs (store, API, types)Code specific to one MFE only
You want type safety and IDE autocompletionRuntime flexibility is more important
Monorepo with Turborepo or npm workspacesPolyrepo (separate Git repositories per MFE)

Pattern 2: Server-Side Integration (SSR / ISR)

In a Next.js MFE architecture, the server can pre-render pages before sending HTML to the browser. This includes Static Site Generation (SSG), Server-Side Rendering (SSR), and Incremental Static Regeneration (ISR) — where pages are statically generated but automatically refreshed at intervals.

How It Works

  1. Next.js MFE uses getStaticProps with revalidate for ISR
  2. Page is generated as static HTML at build time
  3. After the revalidate interval (e.g., 300 seconds), Next.js regenerates the page in the background
  4. Users always get fast static HTML — no loading spinner
  5. A CMS can trigger on-demand revalidation via an API route

Code Example

apps/host/pages/shop/[category].tsx
// apps/host/pages/shop/[category].tsx
// Next.js Incremental Static Regeneration (ISR)
export async function getStaticProps({ params }) {
  const products = await fetchProductsByCategory(params.category)

  return {
    props: { products },
    revalidate: 300, // Re-generate page every 5 minutes
  }
}

// Result: Page is statically generated at build time
// After 5 minutes, Next.js regenerates the page in the background
// Users always get fast static HTML — no loading spinner

When to Use

Use WhenAvoid When
SEO-critical pages (product listings, landing pages)Client-only interactive pages (cart, checkout)
Content that changes infrequently (product catalog)Real-time data that changes per user
You want fast initial page load (no JavaScript needed)Pages that require authentication to render

Not all MFEs need SSR. In a mixed architecture, Next.js MFEs (Products, Content) use SSR/ISR for SEO, while React MFEs (Auth, Cart, Account) are client-only because they require user authentication to render meaningful content. Use the right rendering strategy per domain.

Pattern 3: Runtime Integration via iframes

Each MFE is loaded inside an <iframe> element. The MFEs are completely isolated — separate DOM, separate JavaScript context, separate CSS.

Code Example

iframe integration (NOT recommended)
<!-- Pattern 3: iframe integration (NOT recommended) -->
<iframe
  src="https://products.example.com/catalog"
  width="100%"
  height="600"
  style="border: none;"
  title="Product Catalog"
/>

<!-- Problems:
  - No shared state (Redux store is separate)
  - No shared auth (login required separately in each iframe)
  - Poor performance (each iframe loads full React bundle)
  - No CSS consistency (styles don't leak across iframes)
  - Accessibility issues (screen readers struggle with iframes)
  - SEO invisible (Google cannot crawl iframe content)
-->

Why This Pattern Is NOT Recommended

ProblemImpact
No shared Redux storeCart MFE cannot read auth state from Auth MFE
No shared authenticationUser must log in separately in each iframe
Duplicate bundlesEach iframe loads its own React, Redux, etc.
No CSS consistencyStyles don't cross iframe boundaries
Accessibility issuesScreen readers struggle to navigate iframes
SEO invisibleGoogle cannot crawl content inside iframes
Mobile layout issuesiframes don't resize responsively
⚠️

Avoid iframes for Micro Frontend integration. Module Federation solves every problem that iframes have — shared state, shared auth, shared dependencies, consistent styling — while still keeping MFEs independently deployable. The only valid use case for iframes is embedding third-party content you don't control (like a payment gateway or embedded video).

Pattern 4: Runtime Integration via JavaScript

MFE components are loaded at runtime using React.lazy() (React apps) or next/dynamic (Next.js apps). The remote bundle is fetched over the network when needed, and the component renders like a normal React component.

This pattern relies on Module Federation to make remote imports work (see Pattern 5), but the loading mechanism itself is standard React.

Code Example — React Host (React.lazy)

apps/host/src/App.jsx
// Pattern 4: Runtime JavaScript — React.lazy() with error handling
// apps/host/src/App.jsx (React Host application)
import React, { Suspense, lazy } from 'react'

// Each Remote MFE is loaded at runtime via Module Federation
// .catch() provides a fallback if the remote fails to load
const ProductsMFE = lazy(() =>
  import('ProductManagement/ProductsMFE').catch((err) => {
    console.error('Failed to load ProductsMFE', err)
    return { default: () => <div>Failed to load Products Module</div> }
  })
)

const OrdersMFE = lazy(() =>
  import('OrdersManagement/OrdersMFE').catch((err) => {
    console.error('Failed to load OrdersMFE', err)
    return { default: () => <div>Failed to load Orders Module</div> }
  })
)

const AnalyticsMFE = lazy(() =>
  import('AnalyticsManagement/AnalyticsMFE').catch((err) => {
    console.error('Failed to load AnalyticsMFE', err)
    return { default: () => <div>Failed to load Analytics Module</div> }
  })
)

const LoadingFallback = () => (
  <div className="flex items-center justify-center h-screen">
    <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto" />
    <p className="mt-4 text-gray-600">Loading...</p>
  </div>
)

// Usage in routes:
<Suspense fallback={<LoadingFallback />}>
  <ProductsMFE />
</Suspense>

Code Example — Next.js Host (next/dynamic)

apps/host/pages/login/index.tsx
// Pattern 4 in Next.js: next/dynamic with ssr: false
// apps/host/pages/login/index.tsx (Next.js Host application)
import dynamic from 'next/dynamic'
import { Suspense } from 'react'
import { useRouter } from 'next/navigation'

const RemoteLogin = dynamic(
  () => import('Auth/Login'),
  {
    loading: () => <LoadingState />,
    ssr: false,
  }
)

export default function LoginPage() {
  const router = useRouter()

  const handleNavigate = (path, state) => {
    if (state?.mobileNumber) {
      sessionStorage.setItem('mobileNumber', state.mobileNumber)
    }
    router.push(path)
  }

  return (
    <Suspense fallback={<LoadingState />}>
      <RemoteLogin onNavigate={handleNavigate} />
    </Suspense>
  )
}

React.lazy vs next/dynamic

FeatureReact.lazy()next/dynamic
FrameworkReact (Webpack)Next.js
SSR supportNoYes (configurable with ssr: false)
Loading fallbackRequires <Suspense> wrapperBuilt-in loading option
Error handling.catch() on import.catch() on import
Use caseReact-only Host (Webpack devServer)Next.js Host (SSR + pages router)

Always add .catch() to remote imports. If a Remote MFE is down or fails to load, the .catch() handler returns a fallback component instead of crashing the entire Host application. This is critical for production — one MFE failure should not bring down the whole site.

Pattern 5: Module Federation (Webpack 5) — The Recommended Approach

Webpack Module Federation (opens in a new tab) is a Webpack 5 plugin that allows separate builds to share modules at runtime. Each MFE exposes components via a remoteEntry.js file. The Host declares remotes and loads them dynamically. Shared dependencies (React, Redux) are loaded as singletons — one instance for all MFEs.

For Next.js applications, use NextFederationPlugin (opens in a new tab) which extends Module Federation for SSR, page routing, and Next.js-specific asset loading.

How It Works

  1. Each Remote MFE exposes components via remoteEntry.js
  2. The Host declares remotes — URLs pointing to each MFE's remoteEntry.js
  3. At runtime, the Host fetches the remote bundle and renders the component
  4. Shared dependencies (React, Redux, store packages) are loaded once as singletons

Module Federation runtime flow showing Host loading remoteEntry.js from multiple Remote MFEs with shared singleton dependencies

Code Example — Mixed Architecture (Next.js Host + React & Next.js Remotes)

Next.js Host (NextFederationPlugin)

Local Development and Production configs are different. Locally, remotes point to localhost:PORT. On the server, Nginx routes each path to the correct MFE build. Always update remote URLs when switching environments.

apps/host/next.config.js
// apps/host/next.config.js — LOCAL DEVELOPMENT
const { NextFederationPlugin } = require('@module-federation/nextjs-mf')

module.exports = {
  reactStrictMode: false,
  transpilePackages: ['@myapp/seo', '@myapp/store', '@myapp/api'],

  webpack(config, { isServer }) {
    config.plugins.push(
      new NextFederationPlugin({
        name: 'Host',
        filename: 'static/chunks/remoteEntry.js',
        remotes: {
          // Each MFE runs on its own localhost port
          Auth:     'Auth@https://localhost:3001/remoteEntry.js',
          Account:  'Account@https://localhost:3002/remoteEntry.js',
          Cart:     'Cart@https://localhost:3003/remoteEntry.js',
          Content:  'Content@http://localhost:3004/content/_next/static/'
            + (isServer ? 'ssr' : 'chunks') + '/remoteEntry.js',
          Products: 'Products@http://localhost:3005/products/_next/static/'
            + (isServer ? 'ssr' : 'chunks') + '/remoteEntry.js',
          Support:  'Support@https://localhost:3006/remoteEntry.js',
        },
        shared: {
          react:              { singleton: true, requiredVersion: false, eager: false },
          'react-dom':        { singleton: true, requiredVersion: false, eager: false },
          'react-redux':      { singleton: true, requiredVersion: false, eager: false },
          '@reduxjs/toolkit':  { singleton: true, requiredVersion: false, eager: false },
          '@myapp/store':     { singleton: true, strictVersion: true, requiredVersion: '1.0.0' },
          '@myapp/api':       { singleton: true, strictVersion: true, requiredVersion: '1.0.0' },
          '@myapp/seo':       { singleton: true, strictVersion: true, requiredVersion: '1.0.0' },
        },
        extraOptions: {
          exposePages: true,
          enableImageLoaderFix: true,
          enableUrlLoaderFix: true,
          automaticAsyncBoundary: true,
        },
      })
    )
    return config
  },
}

React Remote (ModuleFederationPlugin)

apps/auth/webpack.config.js
// apps/auth/webpack.config.js — LOCAL DEVELOPMENT
const { ModuleFederationPlugin } = require('webpack').container
const dependencies = require('./package.json').dependencies

module.exports = {
  mode: 'development',
  output: {
    publicPath: 'https://localhost:3001/',
    filename: '[name].bundle.js',
  },
  devServer: {
    port: 3001,
    historyApiFallback: true,
    headers: { 'Access-Control-Allow-Origin': '*' },
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'Auth',
      filename: 'remoteEntry.js',
      exposes: {
        './Login':     './src/components/Login.jsx',
        './OTPVerify': './src/components/OTPVerify.jsx',
        './Register':  './src/components/Register.jsx',
      },
      shared: {
        react:              { singleton: true, requiredVersion: dependencies.react, eager: false },
        'react-dom':        { singleton: true, requiredVersion: dependencies['react-dom'], eager: false },
        'react-redux':      { singleton: true, requiredVersion: dependencies['react-redux'], eager: false },
        '@reduxjs/toolkit':  { singleton: true, requiredVersion: dependencies['@reduxjs/toolkit'], eager: false },
        '@myapp/store':     { singleton: true, strictVersion: true, requiredVersion: '1.0.0' },
        '@myapp/api':       { singleton: true, strictVersion: true, requiredVersion: '1.0.0' },
      },
    }),
  ],
  optimization: { splitChunks: false },
}

Next.js Remote (NextFederationPlugin)

apps/products/next.config.js
// apps/products/next.config.js — Next.js Remote with NextFederationPlugin
const NextFederationPlugin = require('@module-federation/nextjs-mf')

module.exports = {
  reactStrictMode: true,
  output: 'standalone',
  basePath: '/products',
  assetPrefix: '/products',
  transpilePackages: ['@myapp/seo', '@myapp/store', '@myapp/api'],

  webpack: (config, options) => {
    config.plugins.push(
      new NextFederationPlugin({
        name: 'Products',
        filename: 'static/chunks/remoteEntry.js',
        exposes: {
          './ProductList':       './components/ProductList.tsx',
          './ProductDetailPage': './pages/products/index.tsx',
        },
        shared: {
          react:              { singleton: true, requiredVersion: false, eager: false },
          'react-dom':        { singleton: true, requiredVersion: false, eager: false },
          'react-redux':      { singleton: true, requiredVersion: false, eager: false },
          '@reduxjs/toolkit':  { singleton: true, requiredVersion: false, eager: false },
          '@myapp/store':     { singleton: true, strictVersion: true, requiredVersion: '1.0.0' },
          '@myapp/api':       { singleton: true, strictVersion: true, requiredVersion: '1.0.0' },
          '@myapp/seo':       { singleton: true, strictVersion: true, requiredVersion: '1.0.0' },
        },
        extraOptions: {
          automaticAsyncBoundary: true,
        },
      })
    )
    return config
  },
}

Next.js Remotes require basePath and assetPrefix. These ensure all static assets and routes are served under the correct sub-path (e.g., /products/). This config stays the same for local and production — only the Host's remote URLs change.

React Host (ModuleFederationPlugin)

apps/host/webpack.config.js
// apps/host/webpack.config.js — LOCAL DEVELOPMENT
const { ModuleFederationPlugin } = require('webpack').container
const dependencies = require('./package.json').dependencies

module.exports = {
  mode: 'development',
  output: {
    publicPath: '/',
    filename: '[name].bundle.js',
  },
  devServer: {
    port: 4000,
    historyApiFallback: true,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
      'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
    },
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'Host',
      filename: 'remoteEntry.js',
      remotes: {
        // Each MFE on its own localhost port
        ProductManagement:   'ProductManagement@https://localhost:4002/remoteEntry.js',
        InventoryManagement: 'InventoryManagement@https://localhost:4003/remoteEntry.js',
        OrdersManagement:    'OrdersManagement@https://localhost:4004/remoteEntry.js',
        PricingManagement:   'PricingManagement@https://localhost:4005/remoteEntry.js',
        AnalyticsManagement: 'AnalyticsManagement@https://localhost:4007/remoteEntry.js',
        SettingsManagement:  'SettingsManagement@https://localhost:4009/remoteEntry.js',
      },
      shared: {
        react:              { singleton: true, requiredVersion: dependencies.react },
        'react-dom':        { singleton: true, requiredVersion: dependencies['react-dom'] },
        'react-router-dom': { singleton: true, requiredVersion: dependencies['react-router-dom'] },
        '@reduxjs/toolkit':  { singleton: true, requiredVersion: '^2.6.0' },
        'react-redux':      { singleton: true, requiredVersion: '^9.2.0' },
        '@myapp/store':     { singleton: true, requiredVersion: '0.0.1' },
      },
    }),
  ],
}

Key Differences: Local vs Production

SettingLocal DevelopmentProduction / Server
modedevelopmentproduction
Remote URLs (React)https://localhost:4002/remoteEntry.js/productmanagement/remoteEntry.js
Remote URLs (Next.js)http://localhost:3005/products/_next/static/chunks/remoteEntry.js/products/_next/static/chunks/remoteEntry.js
publicPath (React Remote)https://localhost:PORT//app-name/
filename[name].bundle.js[name].[contenthash].js
devServerEnabled (port, CORS headers)Not used
splitChunksDisabled (false)Enabled (vendor chunks)

NextFederationPlugin vs ModuleFederationPlugin

FeatureModuleFederationPluginNextFederationPlugin
SourceBuilt into Webpack 5@module-federation/nextjs-mf package
Use withReact apps (Webpack devServer)Next.js apps (pages router)
SSR supportNoYes (separate ssr/ and chunks/ remoteEntry)
remoteEntry path/remoteEntry.js/static/chunks/remoteEntry.js (client)
basePath/assetPrefixNot neededRequired for Next.js sub-path routing
transpilePackagesNot neededRequired for shared workspace packages
extraOptionsNot availableexposePages, enableImageLoaderFix, automaticAsyncBoundary

Mixing React and Next.js remotes is fully supported. A Next.js Host can load both Next.js remotes (Products, Content — with basePath and assetPrefix) and plain React remotes (Auth, Cart, Account — with standard publicPath). The Host's remotes config uses different URL patterns for each type.

Key Configuration Rules

  1. singleton: true on all shared dependencies — prevents duplicate React instances
  2. strictVersion: true on shared workspace packages — catches version mismatches immediately
  3. eager: false on shared deps — loaded on demand, not upfront
  4. basePath + assetPrefix on Next.js remotes — required for sub-path routing (e.g., /products/)
  5. isServer ? 'ssr' : 'chunks' in remote URLs — Next.js needs different remoteEntry for SSR vs client

Which Pattern Should You Choose?

Micro Frontend integration pattern decision tree showing when to use each of the 5 patterns based on project requirements

SituationRecommended Pattern
Shared Redux store, API client, typesPattern 1: Build-Time (npm packages)
SEO-critical product pagesPattern 2: Server-Side (ISR/SSR)
Embedding third-party content you don't controlPattern 3: iframes (only valid use)
Loading MFE components on demandPattern 4: Runtime JavaScript
Full MFE architecture with independent deploysPattern 5: Module Federation
Production e-commerce with mixed React + Next.jsPattern 1 + 4 + 5 combined

The Recommended Stack

Build-Time packages (Pattern 1) for shared utilities + Module Federation (Pattern 5) for runtime integration + Runtime JavaScript (Pattern 4) for lazy loading = the most battle-tested MFE architecture for production applications.

For official documentation, see the Webpack Module Federation docs (opens in a new tab), NextFederationPlugin on npm (opens in a new tab), and Turborepo docs (opens in a new tab).

Summary

PatternIndependenceShared StatePerformanceSEORecommended?
Build-TimeLow (rebuild needed)YesExcellentN/AYes (for shared code)
Server-SideMediumLimitedExcellentExcellentYes (for SEO pages)
iframesHighNoPoorNoNo (avoid)
Runtime JSHighVia shared storeGoodDependsYes (with Module Fed)
Module FederationHighYes (singleton)ExcellentYes (with SSR)Yes (primary)

What's Next?

Now that you understand the five integration patterns, the next step is learning how MFEs communicate with each other — shared state, callback props, URL routing, and shared API layers.

← Back to Micro Frontend vs Monolith

Continue to Micro Frontend Communication Patterns →


Frequently Asked Questions

What are the different Micro Frontend integration patterns?

There are 5 main integration patterns: Build-Time Integration (npm packages shared via workspaces), Server-Side Integration (SSR/ISR with Next.js), Runtime via iframes (isolated but limited), Runtime via JavaScript (React.lazy or next/dynamic loading remote bundles), and Module Federation (Webpack 5 plugin that loads remote components at runtime with shared dependencies).

Why is Module Federation the best approach for Micro Frontends?

Module Federation allows MFEs to share dependencies like React and Redux as singletons at runtime — avoiding duplicate bundles. Each MFE deploys independently with its own remoteEntry.js file. The Host loads remote components on demand without rebuilding. Combined with shared npm workspace packages for state and API layers, it provides the best balance of independence and shared functionality.

What is the difference between NextFederationPlugin and ModuleFederationPlugin?

ModuleFederationPlugin is Webpack 5's built-in plugin for standard React apps. NextFederationPlugin from @module-federation/nextjs-mf extends it for Next.js — handling SSR chunks, page routing, static asset loading, and the distinction between server-side (ssr/) and client-side (chunks/) remoteEntry.js files. Use NextFederationPlugin when your Host or Remote is a Next.js application.

Why should you avoid iframes for Micro Frontend integration?

Iframes create completely isolated contexts — no shared Redux state, no shared authentication, no shared CSS. Each iframe loads a full React bundle independently, causing poor performance. Screen readers struggle with iframe content, and Google cannot crawl iframe-loaded content for SEO. Module Federation solves all of these problems while still keeping MFEs independently deployable.

Can you mix React and Next.js Micro Frontends in the same application?

Yes. A Next.js Host can load both Next.js remotes (using NextFederationPlugin with basePath and assetPrefix) and plain React remotes (using standard ModuleFederationPlugin). Next.js remotes have a different remoteEntry.js path pattern (_next/static/chunks/ for client, _next/static/ssr/ for server) while React remotes serve remoteEntry.js directly from their publicPath.