Overview
Module Federation (Webpack 5) lets multiple independently-deployed apps share code at runtime — no npm publish cycle, no build-time coupling. One team ships a button update; every consumer gets it on the next page load.
Core concepts:
- Remote — an app that exposes components/modules (
exposesconfig) - Host — an app that consumes remote modules (
remotesconfig) - Shared — dependencies (React, React-DOM) that must be singletons across the runtime
When it earns its complexity:
- Multiple teams deploying independent frontend apps
- Shared design system or auth components that change frequently
- You want consumers updated without a build cycle
When to avoid it:
- Single team, single repo — just import the component
- SSR-critical above-fold content (MF is fundamentally client-side; SSR requires extra tooling)
- Teams not disciplined about interface contracts
Technical Implementation
Remote Configuration
// design-system/next.config.js
const NextFederationPlugin = require('@module-federation/nextjs-mf');
module.exports = {
webpack(config) {
config.plugins.push(
new NextFederationPlugin({
name: 'designSystem',
filename: 'static/chunks/remoteEntry.js',
exposes: {
'./Button': './components/Button/index.tsx',
'./NavBar': './components/NavBar/index.tsx',
'./ProductCard': './components/ProductCard/index.tsx',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0', eager: false },
'react-dom': { singleton: true, requiredVersion: '^18.0.0', eager: false },
},
})
);
return config;
},
};
Host Configuration
// product-app/next.config.js
const NextFederationPlugin = require('@module-federation/nextjs-mf');
module.exports = {
webpack(config) {
config.plugins.push(
new NextFederationPlugin({
name: 'productHost',
remotes: {
designSystem: `designSystem@${process.env.DS_REMOTE_URL}/_next/static/chunks/remoteEntry.js`,
auth: `auth@${process.env.AUTH_REMOTE_URL}/_next/static/chunks/remoteEntry.js`,
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
})
);
return config;
},
};
Consuming a Remote — With Fallback
// product-app/components/AddToCart.tsx
import dynamic from 'next/dynamic';
// Always provide a fallback — remotes WILL 503 in production
const RemoteButton = dynamic(
() =>
import('designSystem/Button').catch(() => {
return import('./FallbackButton'); // local fallback component
}),
{
ssr: false,
loading: () => <div className="h-10 w-32 animate-pulse bg-secondary rounded-md" />,
}
);
export function AddToCartButton({ productId }: { productId: string }) {
return (
<RemoteButton variant="primary" onClick={() => addToCart(productId)}>
Add to Cart
</RemoteButton>
);
}
TypeScript Contract Types
MF has no build-time type checking for remotes. The pattern that worked:
// @platform/mf-contracts (shared package, published on each remote release)
declare module 'designSystem/Button' {
interface ButtonProps {
variant: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick?: () => void;
children: React.ReactNode;
}
const Button: React.FC<ButtonProps>;
export default Button;
}
Architecture
Challenges
1. React singleton violations
Early on, a remote shipped react@18.3.0 while the host was on 18.2.0. With singleton: true, Webpack chose 18.3.0. Hooks broke in the host because it thought it was running two React instances (a known React constraint).
Fix: CI check that fails a remote deploy if its react version differs from the platform's pinned version by more than a patch.
2. SSR hydration mismatches
Remote components rendered server-side via @module-federation/node but the client received a different version of the remote. Hydration failed silently, causing layout jumps.
Fix: disabled SSR for all MF components (ssr: false). Shipped skeletons for above-fold content. Accepted the CLS cost as acceptable for the operational simplicity.
3. remoteEntry.js cache invalidation
With Cloudfront aggressive caching, remoteEntry.js was served stale for up to 24h after a remote deploy. Consumers ran old code silently.
Fix: added remoteEntry.js?v=${BUILD_ID} as a cache-busting query param, configured in the host's environment variable pipeline.
Platform Ownership Checklist
- Define the remote/host topology, shared dependency contracts, and fallback strategy.
- Treat design-system remotes as versioned platform products with explicit compatibility guarantees.
- Publish TypeScript declarations through a shared contracts package such as
@platform/mf-contracts. - Monitor React singleton violations, hydration mismatches, and remote loading failures in production.
- Keep the integration guide, compatibility matrix, and rollback procedure current.
References
- Webpack Module Federation Docs — official spec
- @module-federation/nextjs-mf — Next.js plugin
- Module Federation Examples — official examples
- Zack Jackson's MF Blog — author's overview
- Related: Micro-Frontend Architecture