← Back to Blog

Building Scalable Microfrontends with Module Federation

When our team was tasked with building an enterprise contact center application to replace a legacy third-party tool, we knew the scale would be massive. The application would eventually be maintained by 30+ engineers working across multiple modules simultaneously. A monolithic frontend simply wouldn't cut it.

We needed an architecture that would allow teams to develop, test, and deploy their modules independently — without stepping on each other's toes. That's where Webpack Module Federation came in.

Why Microfrontends?

The decision wasn't made lightly. Microfrontends add complexity — there's no denying that. But the benefits for our specific use case were compelling:

  • Independent deployments — Teams could ship features without coordinating release schedules
  • Technology flexibility — Different modules could upgrade dependencies at their own pace
  • Smaller codebases — Each module was easier to understand and maintain
  • Team autonomy — Clear ownership boundaries reduced merge conflicts and coordination overhead

Module Federation: The Architecture

Webpack 5's Module Federation plugin lets you load remote modules at runtime. Think of it as a way for independently built and deployed applications to share code dynamically.

Our architecture consisted of:

  • A host shell that handled routing, authentication, and layout
  • Multiple remote modules — each owned by a different team (call management, agent dashboard, reporting, etc.)
  • A shared component library exposed via Module Federation to ensure UI consistency

Here's a simplified look at the host configuration:

// webpack.config.js (Host)
new ModuleFederationPlugin({
  name: 'shell',
  remotes: {
    callModule: 'callModule@/call/remoteEntry.js',
    dashboard: 'dashboard@/dashboard/remoteEntry.js',
    reporting: 'reporting@/reports/remoteEntry.js',
  },
  shared: {
    react: { singleton: true, requiredVersion: '^18.0.0' },
    'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
    'react-router-dom': { singleton: true },
  },
})

And the remote configuration:

// webpack.config.js (Remote - Call Module)
new ModuleFederationPlugin({
  name: 'callModule',
  filename: 'remoteEntry.js',
  exposes: {
    './CallPanel': './src/CallPanel',
    './CallHistory': './src/CallHistory',
  },
  shared: {
    react: { singleton: true },
    'react-dom': { singleton: true },
  },
})

Key Lessons Learned

1. Shared Dependencies Need Strict Versioning

The singleton: true flag is crucial for libraries like React that can't have multiple instances. But you also need requiredVersion to prevent subtle runtime bugs when teams drift on dependency versions. We enforced this through a shared ESLint configuration and automated CI checks.

2. Error Boundaries Are Non-negotiable

When a remote module fails to load (network issue, deployment failure, etc.), you don't want it to crash the entire application. We wrapped every remote module import in an error boundary with a graceful fallback UI. This turned potential crashes into minor, recoverable issues.

3. Design a Robust Communication Layer

Microfrontends need to communicate — the call module needs to know about the current agent's state, the dashboard needs to react to call events, etc. We used a combination of:

  • Custom events for loose coupling between modules
  • Shared state store (Redux) for critical global state like auth and user context
  • URL-based state for navigation and deep linking

4. Performance Requires Intentional Effort

Module Federation can hurt performance if you're not careful. Loading multiple remote entry points adds network requests. Here's what we did:

  • Prefetched remote entries for modules likely to be needed next
  • Implemented strategic caching with content-hash filenames and long cache TTLs
  • Used code-splitting within each remote module to avoid shipping unnecessary code
  • Ran bundle analysis regularly to catch size regressions

The result? We reduced initial load time by 40% compared to our first prototype — and that was with a larger feature set.

When to Use (and Not Use) Microfrontends

Microfrontends aren't for every project. They shine when:

  • Multiple teams need to work on the same application independently
  • The application is large enough that a monolith becomes unwieldy
  • Independent deployment cycles are a business requirement

They're probably overkill when:

  • You have a small team (under 5-6 engineers)
  • The application scope is well-defined and unlikely to grow significantly
  • The added infrastructure complexity isn't justified by the team structure

Final Thoughts

Building the contact center with microfrontends was one of the most challenging and rewarding projects I've worked on. The architecture allowed us to scale from 2 engineers to 30+ while maintaining development velocity and code quality.

If you're considering microfrontends for your next project, start with a clear understanding of your team structure and deployment needs. The architecture should serve the team, not the other way around.

The best architecture is one that mirrors your organization's communication structure. — Adapted from Conway's Law