Micro Frontends with Module Federation: Scaling Frontend Architectures for Enterprise
Large companies face a real problem. Their frontend applications grow massive. Multiple teams work on the same codebase. Deployments become risky. New features break existing ones. Sound familiar?
Micro frontends solve this. They let teams build and deploy parts of an application independently. Webpack 5’s Module Federation makes this practical. No more waiting for other teams. No more deployment bottlenecks.
This isn’t just theory. Companies like Spotify, Amazon, and Netflix use micro frontends. They’ve proven it works at scale.
What Are Micro Frontends?
Think of micro frontends like microservices, but for the frontend. Instead of one big application, you have smaller, independent pieces. Each piece can be developed, tested, and deployed separately.
Traditional Monolithic Frontend:
┌─────────────────────────────────────┐
│ Single App │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐│
│ │ Header │ │ Content │ │ Footer ││
│ └─────────┘ └─────────┘ └─────────┘│
│ ┌─────────┐ ┌─────────┐ ┌─────────┐│
│ │ Auth │ │ Orders │ │ Profile ││
│ └─────────┘ └─────────┘ └─────────┘│
│ │
│ All teams work here │
└─────────────────────────────────────┘
Micro Frontend Architecture:
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Auth │ │ Orders │ │ Profile │ │Analytics│
│ Team │ │ Team │ │ Team │ │ Team │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
│ │ │ │
└───────────┼───────────┼───────────┘
│ │
┌─────────┴───────────┴─────────┐
│ Host App │
│ (Shell/Container) │
└───────────────────────────────┘
Each micro frontend is a complete application. It has its own code, tests, and deployment pipeline. Teams can choose their own tech stack. React team uses React. Vue team uses Vue. Angular team uses Angular.
Why Enterprises Are Adopting Micro Frontends
Team Independence
Large companies have multiple frontend teams. In a monolithic app, these teams step on each other. One team’s change breaks another team’s feature. Deployments require coordination across all teams.
Micro frontends fix this. Each team owns their part completely. They can deploy whenever they want. They can use whatever framework they prefer. They can move at their own speed.
Reduced Risk
In a monolithic app, every deployment is risky. A small bug in one feature can break the entire application. With micro frontends, problems stay contained. If the orders team breaks something, only the orders section breaks.
Technology Diversity
Different teams can use different technologies. The authentication team might prefer React. The dashboard team might prefer Vue. The reporting team might prefer Angular. This is fine with micro frontends.
Scalability
As companies grow, they can add more teams. Each new team gets their own micro frontend. They don’t need to understand the entire codebase. They just need to understand their piece.
How Module Federation Works
Module Federation is a Webpack 5 feature. It lets applications share code at runtime. One application can consume components from another application.
Host and Remote Apps
In Module Federation, you have two types of applications:
Host App: The main application that loads other applications Remote App: An application that exposes components to other applications
Module Federation Architecture:
Remote App (Orders)
┌─────────────────────┐
│ webpack.config.js │
│ exposes: { │
│ './OrderList': │
│ './OrderDetail' │
│ } │
└─────────────────────┘
│
│ exposes components
│
Host App (Shell)
┌─────────────────────┐
│ webpack.config.js │
│ remotes: { │
│ 'orders': │
│ 'orders@http...' │
│ } │
└─────────────────────┘
│
│ consumes components
│
┌─────────┐
│ Browser │
└─────────┘
Shared Dependencies
Module Federation can share dependencies between applications. This prevents duplicate code and reduces bundle size.
// Shared dependencies configuration
new ModuleFederationPlugin({
shared: {
react: {
singleton: true,
requiredVersion: '^18.0.0'
},
'react-dom': {
singleton: true,
requiredVersion: '^18.0.0'
}
}
})
Lazy Loading
Remote components are loaded on demand. The host app doesn’t download the remote code until it’s needed. This improves initial load time.
Real-World Architecture Example
Let’s look at an e-commerce platform with micro frontends:
E-commerce Micro Frontend Architecture:
┌─────────────────────────────────────────────────────────┐
│ Host App (Shell) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Header │ │ Navigation │ │ Footer │ │
│ │ Component │ │ Component │ │ Component │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
│ │ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ Auth │ │ Orders │ │ Profile │ │Analytics│
│ Team │ │ Team │ │ Team │ │ Team │
│ │ │ │ │ │ │ │
│ React │ │ Vue │ │ Angular │ │ React │
│ 18 │ │ 3 │ │ 15 │ │ 18 │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
Each team owns their micro frontend completely. They can deploy independently. They can use different frameworks. They can have different release cycles.
Deployment Pipeline
Independent Deployment Pipelines:
Auth Team Pipeline:
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Code │───▶│ Test │───▶│ Build │───▶│ Deploy │
│ Commit │ │ Suite │ │ Bundle │ │ to │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
│
▼
┌─────────┐
│ CDN │
│ Storage │
└─────────┘
Orders Team Pipeline:
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Code │───▶│ Test │───▶│ Build │───▶│ Deploy │
│ Commit │ │ Suite │ │ Bundle │ │ to │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
│
▼
┌─────────┐
│ CDN │
│ Storage │
└─────────┘
Each team has their own pipeline. They don’t wait for other teams. They don’t coordinate deployments. They just deploy their changes.
Code Samples
Webpack Configuration for Host App
// webpack.config.js - Host App (Shell)
const ModuleFederationPlugin = require('@module-federation/webpack');
module.exports = {
mode: 'development',
devServer: {
port: 3000,
},
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
orders: 'orders@http://localhost:3001/remoteEntry.js',
profile: 'profile@http://localhost:3002/remoteEntry.js',
analytics: 'analytics@http://localhost:3003/remoteEntry.js',
},
shared: {
react: {
singleton: true,
requiredVersion: '^18.0.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^18.0.0',
},
'react-router-dom': {
singleton: true,
requiredVersion: '^6.0.0',
},
},
}),
],
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react'],
},
},
},
],
},
};
Webpack Configuration for Remote App
// webpack.config.js - Remote App (Orders)
const ModuleFederationPlugin = require('@module-federation/webpack');
module.exports = {
mode: 'development',
devServer: {
port: 3001,
},
plugins: [
new ModuleFederationPlugin({
name: 'orders',
filename: 'remoteEntry.js',
exposes: {
'./OrderList': './src/components/OrderList',
'./OrderDetail': './src/components/OrderDetail',
'./OrderForm': './src/components/OrderForm',
},
shared: {
react: {
singleton: true,
requiredVersion: '^18.0.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^18.0.0',
},
'react-router-dom': {
singleton: true,
requiredVersion: '^6.0.0',
},
},
}),
],
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react'],
},
},
},
],
},
};
Consuming Remote Components in Host App
// src/App.jsx - Host App
import React, { Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
// Lazy load remote components
const OrderList = React.lazy(() => import('orders/OrderList'));
const OrderDetail = React.lazy(() => import('orders/OrderDetail'));
const Profile = React.lazy(() => import('profile/Profile'));
const Analytics = React.lazy(() => import('analytics/Analytics'));
function App() {
return (
<Router>
<div className="app">
<header className="app-header">
<h1>E-commerce Platform</h1>
<nav>
<a href="/orders">Orders</a>
<a href="/profile">Profile</a>
<a href="/analytics">Analytics</a>
</nav>
</header>
<main className="app-main">
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/orders" element={<OrderList />} />
<Route path="/orders/:id" element={<OrderDetail />} />
<Route path="/profile" element={<Profile />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
</Suspense>
</main>
</div>
</Router>
);
}
export default App;
Remote Component Example
// src/components/OrderList.jsx - Remote App (Orders)
import React, { useState, useEffect } from 'react';
const OrderList = () => {
const [orders, setOrders] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Fetch orders from API
fetch('/api/orders')
.then(response => response.json())
.then(data => {
setOrders(data);
setLoading(false);
})
.catch(error => {
console.error('Error fetching orders:', error);
setLoading(false);
});
}, []);
if (loading) {
return <div>Loading orders...</div>;
}
return (
<div className="order-list">
<h2>Your Orders</h2>
{orders.length === 0 ? (
<p>No orders found.</p>
) : (
<div className="orders-grid">
{orders.map(order => (
<div key={order.id} className="order-card">
<h3>Order #{order.id}</h3>
<p>Status: {order.status}</p>
<p>Total: ${order.total}</p>
<p>Date: {new Date(order.date).toLocaleDateString()}</p>
<a href={`/orders/${order.id}`}>View Details</a>
</div>
))}
</div>
)}
</div>
);
};
export default OrderList;
State Management Across Micro Frontends
Sharing state between micro frontends is tricky. Each micro frontend has its own state. You need a way to communicate between them.
Using Custom Events
// Shared state management using custom events
class SharedStateManager {
constructor() {
this.state = {};
this.listeners = new Map();
}
setState(key, value) {
this.state[key] = value;
this.notifyListeners(key, value);
}
getState(key) {
return this.state[key];
}
subscribe(key, callback) {
if (!this.listeners.has(key)) {
this.listeners.set(key, new Set());
}
this.listeners.get(key).add(callback);
}
unsubscribe(key, callback) {
if (this.listeners.has(key)) {
this.listeners.get(key).delete(callback);
}
}
notifyListeners(key, value) {
if (this.listeners.has(key)) {
this.listeners.get(key).forEach(callback => {
callback(value);
});
}
}
}
// Global state manager
window.sharedState = new SharedStateManager();
Using Redux with Module Federation
// shared/store.js - Shared Redux store
import { createStore, combineReducers } from 'redux';
// Shared reducers
const userReducer = (state = null, action) => {
switch (action.type) {
case 'SET_USER':
return action.payload;
case 'CLEAR_USER':
return null;
default:
return state;
}
};
const cartReducer = (state = [], action) => {
switch (action.type) {
case 'ADD_TO_CART':
return [...state, action.payload];
case 'REMOVE_FROM_CART':
return state.filter(item => item.id !== action.payload);
case 'CLEAR_CART':
return [];
default:
return state;
}
};
const rootReducer = combineReducers({
user: userReducer,
cart: cartReducer,
});
export const store = createStore(rootReducer);
// Using shared store in micro frontend
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { store } from 'shared/store';
const OrderList = () => {
const user = useSelector(state => state.user);
const cart = useSelector(state => state.cart);
const dispatch = useDispatch();
const addToCart = (item) => {
dispatch({ type: 'ADD_TO_CART', payload: item });
};
return (
<div>
<h2>Welcome, {user?.name}</h2>
<p>Items in cart: {cart.length}</p>
{/* Order list content */}
</div>
);
};
// Wrap component with store provider
const OrderListWithStore = () => (
<Provider store={store}>
<OrderList />
</Provider>
);
export default OrderListWithStore;
Using RxJS for Reactive State
// shared/state.js - RxJS state management
import { BehaviorSubject } from 'rxjs';
class ReactiveStateManager {
constructor() {
this.subjects = new Map();
}
getSubject(key) {
if (!this.subjects.has(key)) {
this.subjects.set(key, new BehaviorSubject(null));
}
return this.subjects.get(key);
}
setState(key, value) {
this.getSubject(key).next(value);
}
getState(key) {
return this.getSubject(key).value;
}
subscribe(key, callback) {
return this.getSubject(key).subscribe(callback);
}
}
// Global reactive state manager
window.reactiveState = new ReactiveStateManager();
// Using reactive state in micro frontend
import React, { useState, useEffect } from 'react';
import { reactiveState } from 'shared/state';
const OrderList = () => {
const [user, setUser] = useState(null);
const [cart, setCart] = useState([]);
useEffect(() => {
// Subscribe to user state changes
const userSubscription = reactiveState.subscribe('user', setUser);
const cartSubscription = reactiveState.subscribe('cart', setCart);
return () => {
userSubscription.unsubscribe();
cartSubscription.unsubscribe();
};
}, []);
const addToCart = (item) => {
const currentCart = reactiveState.getState('cart') || [];
reactiveState.setState('cart', [...currentCart, item]);
};
return (
<div>
<h2>Welcome, {user?.name}</h2>
<p>Items in cart: {cart.length}</p>
{/* Order list content */}
</div>
);
};
export default OrderList;
Best Practices & Pitfalls
Dependency Management
Problem: Different micro frontends might use different versions of the same library.
Solution: Use shared dependencies in Module Federation configuration.
// Good: Share common dependencies
new ModuleFederationPlugin({
shared: {
react: {
singleton: true,
requiredVersion: '^18.0.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^18.0.0',
},
},
});
// Bad: Don't share everything
new ModuleFederationPlugin({
shared: {
// This can cause conflicts
'lodash': true,
'moment': true,
},
});
Versioning Strategies
Problem: How do you handle updates to shared components?
Solution: Use semantic versioning and backward compatibility.
// Version your remote entries
new ModuleFederationPlugin({
name: 'orders',
filename: 'remoteEntry.js',
exposes: {
'./OrderList': './src/components/OrderList',
},
// Add version to filename for cache busting
filename: 'remoteEntry.[contenthash].js',
});
When NOT to Use Micro Frontends
Micro frontends aren’t always the right choice. Don’t use them if:
Small Team: If you have one or two frontend developers, micro frontends add complexity without benefits.
Simple Application: If your app is straightforward and doesn’t change often, a monolithic approach is simpler.
Tight Coupling: If your components are tightly coupled and can’t be separated, micro frontends won’t help.
Performance Critical: The overhead of loading multiple bundles can hurt performance for simple applications.
Limited Resources: Micro frontends require more infrastructure and tooling. Make sure you have the resources to support them.
Common Pitfalls
Over-Engineering: Don’t create micro frontends for every component. Start with logical business boundaries.
State Management Complexity: Sharing state between micro frontends is hard. Plan your state management strategy carefully.
Bundle Size: Without careful dependency management, you can end up with duplicate code and large bundles.
Testing Complexity: Testing interactions between micro frontends is more complex than testing a monolithic app.
Deployment Coordination: While deployments are independent, you still need to coordinate breaking changes.
Conclusion
Micro frontends with Module Federation solve real problems in large organizations. They enable team independence, reduce deployment risk, and allow technology diversity. But they’re not a silver bullet.
The key is to start simple. Don’t try to micro-frontend everything at once. Start with one team and one micro frontend. Learn from the experience. Then expand gradually.
Module Federation makes micro frontends practical. It handles the complex parts like dependency sharing and lazy loading. But you still need to think about state management, testing, and deployment strategies.
The future of frontend development is moving toward more modular architectures. Companies that can break down their monolithic frontends will be more agile and competitive. Module Federation provides the tools to make this transition.
Start small. Learn fast. Scale gradually. That’s how you succeed with micro frontends.
The question isn’t whether micro frontends are the future. The question is whether you’re ready to embrace the change.
Join the Discussion
Have thoughts on this article? Share your insights and engage with the community.