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.

Building Modern Web Applications: Lessons Learned from Implementing Module Federation

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

  1. Always have fallback mechanisms for Module Federation loading
  2. Implement comprehensive error boundaries for each MFE
  3. Use TypeScript strictly across all applications
  4. Centralize shared dependencies in packages
  5. Implement proper environment isolation for team development
  6. Document everything - especially the integration points
  7. Test cross-MFE functionality thoroughly
  8. 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.

Liked it ?

Read more