Micro Frontend Architecture Part 2: Implementation Guide and Production Patterns

In Part 1, I broke down when Micro Frontends make sense and when they're architectural overkill. We covered the decision framework, performance trade-offs, and red flags to watch for.
Now comes the hard part: actually building them.
During my work at Accenture, I spent months evaluating different implementation patterns large React applications. I tested Module Federation, Single-SPA, different state management approaches, and deployment strategies.
This is the guide I wish I had when I started. No fluff, just production-ready patterns with real code.
If you've decided micro frontends are right for your team (and you've checked all the boxes from Part 1), here's exactly how to implement them without destroying your developer experience.
The Right Way to Implement Micro Frontends
There are two main patterns in the React ecosystem:
Module Federation (Webpack 5+) — Recommended for React/Next.js
Single-SPA — Framework agnostic, use when mixing React/Vue/Angular
Let's start with Module Federation since 90% of teams should use this.
Architecture Pattern 1: Module Federation (The Industry Standard)
Webpack Module Federation is the most mature solution for React-based micro frontends. It allows runtime code sharing between independently deployed apps.
How It Works
Three key concepts:
Shell App: The container that orchestrates everything (navigation, authentication, global state)
Remote Apps: Individual micro frontends exposed as federated modules
Shared Dependencies: React, React-DOM, common libraries loaded once (not duplicated)
Real-World Architecture
Here's how I structured a micro frontend system for a large analytics platform:
┌─────────────────────────────────────────────────────┐
│ Shell App (Port 3000) │
│ - Routing │
│ - Authentication │
│ - Global Layout │
│ - Remote orchestration │
└─────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Analytics │ │ Billing │ │ Settings │
│ (Port 3001) │ │ (Port 3002) │ │ (Port 3003) │
│ │ │ │ │ │
│ - Dashboard │ │ - Invoices │ │ - Profile │
│ - Reports │ │ - Payment │ │ - Teams │
│ - Insights │ │ - Usage │ │ - Security │
└──────────────┘ └──────────────┘ └──────────────┘
Step 1: Configure the Remote App (Analytics Micro Frontend)
// webpack.config.js for Analytics Micro Frontend
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const path = require('path');
module.exports = {
entry: './src/index.js',
mode: 'development',
devServer: {
port: 3001,
historyApiFallback: true,
},
output: {
publicPath: 'auto',
},
plugins: [
new ModuleFederationPlugin({
name: 'analytics',
filename: 'remoteEntry.js',
exposes: {
'./AnalyticsDashboard': './src/AnalyticsDashboard',
'./ReportsPage': './src/ReportsPage',
'./InsightsPage': './src/InsightsPage',
},
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0',
eager: true
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
eager: true
},
'react-router-dom': {
singleton: true,
requiredVersion: '^6.8.0'
},
},
}),
],
// ... rest of webpack config
};
Key Configuration Details:
name: 'analytics'— Unique identifier for this remotefilename: 'remoteEntry.js'— Entry point the shell will loadexposes— What components this remote makes availableshared— Dependencies shared with shell (prevents duplication)singleton: true— Only one instance of React loaded across all appseager: true— Load immediately (prevents async loading issues)
Step 2: Configure the Shell App
// webpack.config.js for Shell App
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
entry: './src/index.js',
mode: 'development',
devServer: {
port: 3000,
historyApiFallback: true,
},
output: {
publicPath: 'auto',
},
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
analytics: 'analytics@http://localhost:3001/remoteEntry.js',
billing: 'billing@http://localhost:3002/remoteEntry.js',
settings: 'settings@http://localhost:3003/remoteEntry.js',
},
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0',
eager: true
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
eager: true
},
'react-router-dom': {
singleton: true,
requiredVersion: '^6.8.0'
},
},
}),
],
// ... rest of webpack config
};
Production Configuration:
// webpack.config.prod.js for Shell App
remotes: {
analytics: 'analytics@https://analytics.yourapp.com/remoteEntry.js',
billing: 'billing@https://billing.yourapp.com/remoteEntry.js',
settings: 'settings@https://settings.yourapp.com/remoteEntry.js',
}
Step 3: Dynamic Remote Loading in Shell
// src/App.tsx (Shell App)
import React, { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import ErrorBoundary from './components/ErrorBoundary';
import LoadingSpinner from './components/LoadingSpinner';
// Lazy load micro frontends
const AnalyticsDashboard = lazy(() => import('analytics/AnalyticsDashboard'));
const BillingPage = lazy(() => import('billing/BillingPage'));
const SettingsPage = lazy(() => import('settings/SettingsPage'));
function App() {
return (
<Router>
<div className="app-shell">
<Navigation />
<main className="content">
<ErrorBoundary>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/analytics/*" element={<AnalyticsDashboard />} />
<Route path="/billing/*" element={<BillingPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
</Routes>
</Suspense>
</ErrorBoundary>
</main>
</div>
</Router>
);
}
export default App;
Critical Pattern: Always wrap remote components in both ErrorBoundary and Suspense. If a remote fails to load, the shell should gracefully handle it.
Critical Implementation Challenges (And Solutions)
Challenge 1: State Management Across Micro Frontends
Problem: User logs in on dashboard, settings page needs to know who they are. How do micro frontends communicate?
Solution 1 - Custom Events (Simple, Works for Basic Cases):
// Shared event types
export const USER_LOGIN = 'user:login';
export const USER_LOGOUT = 'user:logout';
export const THEME_CHANGE = 'theme:change';
// Micro Frontend A - Emit event
function login(userData) {
window.dispatchEvent(
new CustomEvent(USER_LOGIN, {
detail: { userId: userData.id, name: userData.name, email: userData.email }
})
);
}
// Micro Frontend B - Listen for event
import { useEffect, useState } from 'react';
function useGlobalUser() {
const [user, setUser] = useState(null);
useEffect(() => {
const handleUserLogin = (e) => setUser(e.detail);
const handleUserLogout = () => setUser(null);
window.addEventListener(USER_LOGIN, handleUserLogin);
window.addEventListener(USER_LOGOUT, handleUserLogout);
return () => {
window.removeEventListener(USER_LOGIN, handleUserLogin);
window.removeEventListener(USER_LOGOUT, handleUserLogout);
};
}, []);
return user;
}
// Usage in any micro frontend
function AnalyticsDashboard() {
const user = useGlobalUser();
if (!user) return <LoginPrompt />;
return <Dashboard userId={user.userId} />;
}
Solution 2 - Shared State Store (Advanced, Recommended for Complex Apps):
// Shared package: @myapp/shared-state
// packages/shared-state/src/store.ts
import create from 'zustand';
import { persist } from 'zustand/middleware';
interface User {
id: string;
name: string;
email: string;
role: string;
}
interface GlobalState {
user: User | null;
theme: 'light' | 'dark';
setUser: (user: User | null) => void;
setTheme: (theme: 'light' | 'dark') => void;
logout: () => void;
}
export const useGlobalStore = create<GlobalState>()(
persist(
(set) => ({
user: null,
theme: 'light',
setUser: (user) => set({ user }),
setTheme: (theme) => set({ theme }),
logout: () => set({ user: null }),
}),
{
name: 'global-storage',
partialize: (state) => ({
user: state.user,
theme: state.theme
}),
}
)
);
// Usage in Shell App
import { useGlobalStore } from '@myapp/shared-state';
function ShellApp() {
const { user, setUser } = useGlobalStore();
const handleLogin = async (credentials) => {
const userData = await loginAPI(credentials);
setUser(userData);
};
return (
<Router>
{/* ... routes */}
</Router>
);
}
// Usage in Analytics Micro Frontend
import { useGlobalStore } from '@myapp/shared-state';
function AnalyticsDashboard() {
const user = useGlobalStore((state) => state.user);
const theme = useGlobalStore((state) => state.theme);
return (
<div className={`dashboard theme-${theme}`}>
<h1>Welcome, {user?.name}</h1>
{/* analytics content */}
</div>
);
}
Shared as a Federated Module:
// webpack.config.js for shared-state package
new ModuleFederationPlugin({
name: 'sharedState',
filename: 'remoteEntry.js',
exposes: {
'./store': './src/store.ts',
},
shared: ['zustand', 'react'],
});
Best Practice: Keep shared state minimal. Only share:
Authentication (user object, tokens)
Theme preferences
Global config (feature flags, API base URLs)
Everything else should be local to the micro frontend.
Challenge 2: Routing Across Micro Frontends
Problem: User navigates from /dashboard to /analytics. How do routes work without conflicts?
Solution - Shell-Based Routing with Nested Routes:
// Shell App - Top-level routing
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
function ShellApp() {
return (
<Router>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/dashboard/*" element={<DashboardApp />} />
<Route path="/analytics/*" element={<AnalyticsApp />} />
<Route path="/billing/*" element={<BillingApp />} />
</Routes>
</Router>
);
}
// Analytics Micro Frontend - Sub-routes
import { Routes, Route } from 'react-router-dom';
export default function AnalyticsApp() {
return (
<Routes>
<Route index element={<AnalyticsOverview />} />
<Route path="reports" element={<ReportsPage />} />
<Route path="reports/:reportId" element={<ReportDetailPage />} />
<Route path="insights" element={<InsightsPage />} />
<Route path="*" element={<NotFound />} />
</Routes>
);
}
Navigation Between Micro Frontends:
// From Dashboard to Analytics
import { useNavigate } from 'react-router-dom';
function DashboardWidget() {
const navigate = useNavigate();
return (
<button onClick={() => navigate('/analytics/reports')}>
View Full Report
</button>
);
}
Programmatic Navigation with State:
// Pass data between micro frontends via navigation state
navigate('/analytics/reports', {
state: {
dateRange: { start: '2024-01-01', end: '2024-01-31' }
}
});
// In Analytics app
import { useLocation } from 'react-router-dom';
function ReportsPage() {
const location = useLocation();
const dateRange = location.state?.dateRange;
return <Reports initialDateRange={dateRange} />;
}
Challenge 3: Shared UI Component Library
Problem: You need consistent buttons, inputs, modals across all micro frontends without duplicating code.
Wrong Approach: Each micro frontend bundles its own copy → 200KB+ duplication.
Right Approach: Share the component library as a federated module.
// Component Library - webpack.config.js
// packages/design-system/webpack.config.js
new ModuleFederationPlugin({
name: 'designSystem',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
'./Input': './src/components/Input',
'./Modal': './src/components/Modal',
'./Card': './src/components/Card',
'./Select': './src/components/Select',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
});
// Usage in any micro frontend
import { Button, Input, Modal } from 'designSystem/components';
function LoginForm() {
return (
<Modal>
<Input placeholder="Email" />
<Input type="password" placeholder="Password" />
<Button>Login</Button>
</Modal>
);
}
Version Control Strategy:
// Pin specific version in production
remotes: {
designSystem: 'designSystem@https://cdn.yourapp.com/design-system/v1.2.3/remoteEntry.js',
}
// Development - use local version
remotes: {
designSystem: process.env.NODE_ENV === 'development'
? 'designSystem@http://localhost:4000/remoteEntry.js'
: 'designSystem@https://cdn.yourapp.com/design-system/v1.2.3/remoteEntry.js',
}
Challenge 4: Performance and Bundle Size Optimization
Problem: Each micro frontend adds network overhead and bundle size.
Solution 1 - Aggressive Code Splitting:
// Lazy load heavy dependencies
const HeavyChart = lazy(() => import('./components/HeavyChart'));
const DataTable = lazy(() => import('./components/DataTable'));
function AnalyticsDashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>Show Chart</button>
{showChart && (
<Suspense fallback={<Skeleton />}>
<HeavyChart />
</Suspense>
)}
</div>
);
}
Solution 2 - Prefetching and Preloading:
// Prefetch on hover for instant navigation
import { Link } from 'react-router-dom';
function Navigation() {
const prefetchAnalytics = () => {
// Webpack magic comment for prefetching
import(/* webpackPrefetch: true */ 'analytics/AnalyticsDashboard');
};
return (
<nav>
<Link
to="/analytics"
onMouseEnter={prefetchAnalytics}
>
Analytics
</Link>
</nav>
);
}
Solution 3 - Bundle Size Monitoring:
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html',
}),
],
performance: {
maxEntrypointSize: 300000, // 300KB limit
maxAssetSize: 300000,
hints: 'error',
},
};
Real-World Implementation Checklist
Here's the exact 10-week roadmap I follow:
Phase 1 - Foundation (Week 1-2)
Week 1: Architecture Planning
[ ] Define clear domain boundaries (1 micro frontend = 1 business domain)
[ ] Map existing features to micro frontend domains
[ ] Identify shared dependencies and create dependency matrix
[ ] Set up mono repo structure or multi-repo strategy
[ ] Create architecture documentation
Week 2: Infrastructure Setup
[ ] Set up Module Federation shell app
[ ] Create shared state package with Zustand
[ ] Set up shared component library as federated module
[ ] Configure webpack for all micro frontends
[ ] Set up local development environment (run all apps simultaneously)
Phase 2 - First Micro Frontend (Week 3-4)
Week 3: Build First Remote
[ ] Choose least critical domain (e.g., Analytics, Reports)
[ ] Build micro frontend with proper error boundaries
[ ] Implement routing within micro frontend
[ ] Integrate with shared state
[ ] Add comprehensive error handling
Week 4: Integration
[ ] Integrate first micro frontend into shell
[ ] Test cross-app navigation
[ ] Implement fallback UI if remote fails
[ ] Set up monitoring (bundle size, load times)
[ ] Deploy to staging environment
Phase 3 - CI/CD and Testing (Week 5-6)
Week 5: Deployment Pipelines
[ ] Set up independent CI/CD for each micro frontend
[ ] Configure versioned deployments
[ ] Implement health checks for each remote
[ ] Set up centralized logging (Sentry, DataDog)
[ ] Create rollback procedures
Week 6: Testing Infrastructure
[ ] Write integration tests across micro frontends
[ ] Set up E2E testing (Cypress, Playwright)
[ ] Implement visual regression testing
[ ] Test failure scenarios (remote unavailable)
[ ] Performance testing and benchmarking
Phase 4 - Scale to All Domains (Week 7-9)
Week 7-9: Migrate Remaining Domains
[ ] Build and integrate 2nd micro frontend
[ ] Build and integrate 3rd micro frontend
[ ] Optimize shared dependencies
[ ] Implement prefetching strategies
[ ] Fine-tune webpack configs for production
Phase 5 - Production Hardening (Week 10)
[ ] Security audit (CSP policies, CORS)
[ ] Accessibility audit across all micro frontends
[ ] Create developer onboarding documentation
[ ] Set up feature flag system
[ ] Final performance optimization
[ ] Production deployment
Common Mistakes I've Seen (And How to Avoid Them)
Mistake 1: Too Many Micro Frontends
Bad: Breaking a 5-page app into 5 micro frontends.
Good: 1 micro frontend per major business domain. For most apps, 3-5 micro frontends is the sweet spot.
Rule of thumb: If a "micro frontend" is less than 3 pages, it shouldn't be separate.
Mistake 2: Sharing Too Much
Bad: Shared utils, shared helpers, shared hooks, shared context, shared everything → You've created a distributed monolith.
Good: Only share authentication, theme, and global config. Each micro frontend should be 80% independent.
// BAD - Over-sharing
import { useDataFetching } from '@shared/hooks';
import { formatDate, calculateTotal } from '@shared/utils';
import { UserContext, ProductContext } from '@shared/contexts';
// GOOD - Minimal sharing
import { useAuth } from '@shared/state'; // Only auth
const formatDate = (date) => { /* local implementation */ };
Mistake 3: Ignoring Developer Experience
Bad: Developers run 4 separate apps to test one feature. Local development is painful.
Good: Mock remote apps in development. Use webpack aliases.
// webpack.config.dev.js
const isDev = process.env.NODE_ENV === 'development';
module.exports = {
plugins: [
new ModuleFederationPlugin({
remotes: {
analytics: isDev && process.env.LOCAL_ANALYTICS
? 'analytics@http://localhost:3001/remoteEntry.js'
: 'analytics@https://analytics.prod.com/remoteEntry.js',
},
}),
],
};
# .env.development
LOCAL_ANALYTICS=true # Use local version
LOCAL_BILLING=false # Use production version
Mistake 4: No Versioning Strategy
Bad: Shell always loads latest version of remotes → Breaking changes crash production.
Good: Pin remote versions explicitly. Use semantic versioning.
// Production - Explicit versions
remotes: {
analytics: 'analytics@https://cdn.app.com/analytics/v1.2.3/remoteEntry.js',
billing: 'billing@https://cdn.app.com/billing/v2.0.1/remoteEntry.js',
}
// Update process:
// 1. Deploy new version (v1.2.4) to CDN
// 2. Test in staging with new version
// 3. Update shell config to point to v1.2.4
// 4. Deploy shell
Mistake 5: Not Handling Remote Failures
Bad: Remote fails to load → Entire app crashes.
Good: Graceful degradation with error boundaries.
// ErrorBoundary.tsx
class RemoteErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, info) {
console.error('Remote loading failed:', error);
// Send to error tracking
logErrorToService(error, info);
}
render() {
if (this.state.hasError) {
return (
<div className="remote-error">
<h2>This section is temporarily unavailable</h2>
<p>We're working on it. Please try again later.</p>
<button onClick={() => window.location.reload()}>
Refresh Page
</button>
</div>
);
}
return this.props.children;
}
}
Architecture Pattern 2: Single-SPA (Framework Agnostic)
Use this only if you need to mix React, Vue, and Angular. Otherwise, stick with Module Federation.
// single-spa-config.ts
import { registerApplication, start } from 'single-spa';
registerApplication({
name: '@myapp/react-dashboard',
app: () => System.import('@myapp/react-dashboard'),
activeWhen: ['/dashboard'],
});
registerApplication({
name: '@myapp/vue-analytics',
app: () => System.import('@myapp/vue-analytics'),
activeWhen: ['/analytics'],
});
registerApplication({
name: '@myapp/angular-billing',
app: () => System.import('@myapp/angular-billing'),
activeWhen: ['/billing'],
});
start();
Why I don't recommend this:
Adds significant complexity
Developer context switching between frameworks
Harder to maintain consistent UX
Tooling fragmentation
When it makes sense: Legacy migration where rewriting isn't feasible.
Final Thoughts
Micro Frontends are not a silver bullet. They're a team scaling solution with real technical costs.
If you've made it this far and you're implementing them, here are my parting recommendations:
Start small: Build one micro frontend first. Validate the pattern works for your team before going all-in.
Measure everything: Track bundle sizes, load times, deployment frequency, team velocity. Make data-driven decisions.
Invest in DX: If local development is painful, developers will hate it. Make the tooling seamless.
Document extensively: Micro frontends have more moving parts. Good docs prevent disasters.
Plan for failures: Remotes will fail. Networks will be slow. Have fallbacks.
The teams that succeed with micro frontends are the ones who treat it as an organizational architecture, not just a technical one.
Did Part 1 and Part 2 give you a complete picture of Micro Frontends? What's still unclear?
If you're implementing this in your team, what's the biggest challenge you're facing? Drop it in the comments — I'd love to help.
Found this guide useful? Share it with other developers evaluating architectural decisions. It might save them months of trial and error.




