Building Modern Web Applications: Lessons Learned from Implementing Module Federation
Lessons learned from implementing Module Federation in a modern web app — from tackling integration hurdles to improving team workflows. Practical tips to help you avoid pitfalls and make the most of a modular architecture.

I was recently playing around to modernize web applications by implementing a micro-frontend (MFE) architecture using Module Federation. This project involved creating a distributed system where multiple independent applications work together to create a seamless user experience. Here are the key lessons learned along the way.
🏗️ Architecture Overview
The application is built around a Shell + MFE architecture with four main components:
- 🏠 Shell Application - The orchestrator that manages the overall flow
- 🔐 Authentication MFE - Handles user authentication and verification
- 💳 Feature MFE - Manages core functionality and processing
- 🛡️ Verification MFE - Handles identity verification and compliance
🎯 Key Lessons Learned
Module Federation Requires Careful React Management
Challenge: Ensuring React is properly available across all MFEs was one of our biggest hurdles.
Solution: Implementing a robust React availability system:
const ensureReactAvailable = () => {
if (typeof window !== "undefined") {
if (!(window as any).React) {
(window as any).React = React;
}
if (!(window as any).ReactDOM) {
import('react-dom').then((ReactDOM) => {
(window as any).ReactDOM = ReactDOM;
});
}
}
};
Lesson: Module Federation requires explicit React sharing, and you need to handle both client and server-side scenarios carefully.
Environment Isolation is Critical
Challenge: Different teams working on different MFEs needed isolated development environments.
Solution: Implementing a sophisticated cookie isolation.
# Development: Port-specific cookies
app-session-token_port_3001
# Staging: Subdomain-specific cookies
app-session-token_staging
# Production: Standard cookies
app-session-token
Lesson: Environment isolation prevents conflicts and allows teams to work independently without affecting each other.
Error Handling Must Be Comprehensive
Challenge: Module Federation loading failures can break the entire application.
Solution: Implementing multiple layers of error handling
const loadAuthMFE = async () => {
try {
// Primary: Module Federation
if ((window as any).__FEDERATION__) {
return await (window as any).__FEDERATION__.instance.loadRemote("auth/AuthMFE");
}
// Fallback: Direct import
return await import("auth/AuthMFE");
} catch (error) {
// Graceful degradation
return {
default: () => <ErrorComponent message={error.message} />
};
}
};
Lesson: Always have fallback mechanisms and graceful degradation strategies.
State Management Across MFEs is Complex
Challenge: Sharing state across multiple independent applications.
Solution: Centralized state management in the shell with careful prop passing:
const mfeProps = {
someState: props.initialData?.someState,
globalConfig: {
someFunc: globalConfig.someFunc,
},
};
Lesson: Design your data flow carefully and ensure all MFEs have access to the data they need.
Build Performance Requires Optimization
Challenge: Building multiple applications with shared dependencies.
Solution: Using Turbo for monorepo
builds and implemented careful dependency management:
{
"build": {
"dependsOn": ["^build", "check-types"],
"inputs": ["src/**", "components/**", "*.config.js"],
"outputs": [".next/**"]
}
}
Lesson: Monorepo
tooling like Turbo is essential for maintaining fast build times.
CSS Architecture Needs Centralization
Challenge: Styling consistency across multiple independent applications.
Solution: Centralized styling in the shell with minimal MFE-specific CSS:
// Shell manages all global styles
import "@acme/ui-core/styles.css";
import "@acme/design-system";
// MFEs have minimal globals.css
Lesson: Centralize as much styling as possible to maintain consistency.
TypeScript Configuration is Critical
Challenge: Type safety across multiple applications with shared types.
Solution: Shared TypeScript configurations and careful type definitions:
{
"extends": "@acme/typescript-config/nextjs.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
"@acme/component-library": ["../../packages/component-library/src"]
}
}
}
Lesson: Invest in proper TypeScript setup early - it pays dividends in maintainability.
Development Workflow Requires Coordination
Challenge: Multiple teams working on different MFEs simultaneously.
Solution: Clear development workflow with team-specific environments:
# Team A uses devA
export NODE_ENV=devA pnpm dev
# Team B uses devB
export NODE_ENV=devB pnpm dev
Lesson: Establish clear development workflows and environment isolation early.
🛠️ Technical Implementation Insights
Module Federation Configuration
Each MFE exposes its main component:
// apps/auth/next.config.js
new NextFederationPlugin({
name: "auth",
filename: "static/chunks/remoteEntry.js",
exposes: {
"./AuthMFE": "./components/AuthMFE.tsx",
},
shared: {
react: { singleton: true, eager: true },
"react-dom": { singleton: true, eager: true },
},
})
Error Boundary Implementation
Implementing comprehensive error boundaries for each MFE:
<ErrorBoundary
onError={(error, errorInfo) => {
console.error("💥 Auth MFE Render Error:", error, errorInfo);
}}
fallback={
<ErrorScreen
title="Authentication Component Error"
message="The authentication component encountered an error."
showIcon={false}
/>
}
>
<AuthComponent {...mfeProps} />
</ErrorBoundary>
📈 Performance Optimizations
Bundle Splitting
Implementing careful bundle splitting to optimize loading:
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10,
},
componentLibrary: {
test: /[\\/]node_modules[\\/]@acme[\\/]component-library[\\/]/,
name: 'component-library',
chunks: 'all',
priority: 20,
},
},
}
Lazy Loading
MFEs are loaded on-demand to improve initial page load:
const AuthMFE = lazy(() => import("auth/AuthMFE"));
const FeatureMFE = lazy(() => import("feature/FeatureMFE"));
🚨 Common Pitfalls and Solutions
1. React Singleton Issues
Problem: Multiple React instances causing hydration errors.
Solution: Ensure React is shared as a singleton across all MFEs.
2. CSS Conflicts
Problem: Styling conflicts between MFEs.
Solution: Centralize global styles and use CSS modules for component-specific styles.
3. Build Performance
Problem: Slow builds with multiple applications.
Solution: Use Turbo for parallel builds and implement proper caching.
4. Development Complexity
Problem: Complex local development setup.
Solution: Create comprehensive scripts and documentation for team onboarding.
🎯 Best Practices Established
- Always have fallback mechanisms for Module Federation loading
- Implement comprehensive error boundaries for each MFE
- Use TypeScript strictly across all applications
- Centralize shared dependencies in packages
- Implement proper environment isolation for team development
- Document everything - especially the integration points
- Test cross-MFE functionality thoroughly
- Monitor performance and bundle sizes regularly
🔮 Future Considerations
As we continue to evolve this architecture, we're considering:
- Runtime Module Federation for even more flexibility
- Advanced caching strategies for better performance
- Automated testing for cross-MFE integration
- Performance monitoring and alerting
- Advanced error tracking and recovery mechanisms
Conclusion
Implementing Module Federation for a Nextjs application was a challenging but rewarding journey. The key to success was:
- Careful planning of the architecture
- Comprehensive error handling at every level
- Proper tooling for development and build processes
- Clear communication and documentation
- Iterative improvement based on real-world usage
The result is a modern, scalable application architecture that allows teams to work independently while maintaining a cohesive user experience. The lessons learned here can be applied to any micro-frontend architecture implementation.