← Back to Transmissions
Architecture

Building Scalable Microfrontends with Module Federation

Nischit Shetty · · 8 min read

JioAssist is an enterprise contact center application built from scratch using React.js and Webpack Module Federation, replacing a legacy third-party tool for a major telecom enterprise. The system uses a microfrontend architecture with independently deployable modules — call management, agent dashboard, reporting, and shared component libraries — integrated with Genesys CTI for telephony controls. The project scaled from 2 engineers to 30, and performance optimizations achieved a 40% reduction in initial load time. This article shares the architectural decisions, hard lessons, and tradeoffs we encountered along the way.

We chose Webpack Module Federation over alternatives like single-spa and iframes because it offered runtime module sharing without sacrificing independent deployment or adding iframe sandboxing complexity. Each team owned a remote module with its own CI/CD pipeline, while a host shell handled routing, authentication, and layout. Here's what we learned.

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 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

Results

After rolling out the microfrontend architecture across all modules:

  • Initial load time reduced by 40% versus our first prototype (with a larger feature set)
  • Team scaled from 2 to 30+ engineers without a linear increase in coordination overhead
  • Release velocity improved — teams shipped features independently on their own schedules
  • Incident blast radius reduced — a broken deployment in one remote no longer crashes other modules

Final Thoughts

Webpack Module Federation is one of the most powerful tools in a frontend architect's toolkit — but it comes with real complexity. Shared dependency versioning, inter-module communication, error isolation, and build-time contracts all need intentional design. If you're building for a team of 10+ engineers with independent deployment requirements, the investment pays off substantially.

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. If a simpler monorepo with feature flags would do, go that route first.

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