Architecting Micro-Frontends with Module Federation: How We Saved $180K/Year
Introduction
When managing complex enterprise frontend systems with multiple independent dev teams, monolithic codebases eventually suffer from build bottlenecks, deployment locks, and cross-team dependency drift. At Powerley, as our frontend platform expanded across customer portals serving 1M+ monthly active users, we faced a choice: migrate to a shared monorepo or adopt a decentralized micro-frontend architecture.
We chose **Webpack Module Federation**.
Why Module Federation over Monorepos?
While monorepos consolidate code under a single repository, they still enforce tied deployment cycles unless heavily customized. Module Federation allowed us to solve these issues via:
- **Independent Team Releases**: 3 cross-functional teams shipped features independently without waiting for unified regression pipelines.
- **Dynamic Runtime Import**: Remote containers loaded code at runtime over CDN rather than compile-time bundler inclusion.
- **Shared Dependency Singletons**: Common vendor packages like `react`, `react-dom`, and `jotai` were loaded once, keeping bundle sizes lean.
How Webpack Module Federation Works
Module Federation works by exposing modules at runtime. Instead of bundler-level bundling, components from remote applications are fetched as standalone containers. A typical Host application configuration in `webpack.config.js` looks like this:
```javascript const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); const deps = require("./package.json").dependencies;
module.exports = { plugins: [ new ModuleFederationPlugin({ name: "host_app", remotes: { billing: "billing_remote@https://cdn.example.com/billing/remoteEntry.js", usage: "usage_remote@https://cdn.example.com/usage/remoteEntry.js" }, shared: { react: { singleton: true, requiredVersion: deps.react }, "react-dom": { singleton: true, requiredVersion: deps["react-dom"] }, jotai: { singleton: true, requiredVersion: deps.jotai } } }) ] }; ```
And the Remote application exposes its entry point:
```javascript module.exports = { plugins: [ new ModuleFederationPlugin({ name: "billing_remote", filename: "remoteEntry.js", exposes: { "./BillingDashboard": "./src/components/BillingDashboard.tsx" }, shared: { react: { singleton: true, requiredVersion: deps.react }, "react-dom": { singleton: true, requiredVersion: deps["react-dom"] } } }) ] }; ```
Singleton Dependency Negotiation
One of the most complex issues in runtime loading is handling dependency version mismatches. If the Host uses React 18.2.0 and the remote requests React 18.3.1, loading both will result in duplicate instances, breaking React contexts and bloating the client bundle.
Module Federation resolves this through dependency negotiation parameters. By specifying `singleton: true`, we force the runtime loader to resolve a single copy. If versions are incompatible, the `fallback` mechanism is triggered, downloading the closest valid semantic version to maintain reliability.
Devops & Edge Infrastructure: Slashing AWS Overhead
Rather than compiling the entire portal on every commit, each micro-frontend is built inside its own GitHub Actions workflow and pushed to Amazon S3. We configure CloudFront to cache the `remoteEntry.js` file with a short TTL (Time to Live) and versioned component chunks with immutable headers (`Cache-Control: max-age=31536000`). This structure bypassed continuous ECS container scaling and Lambda compute workloads on every load, leading to a **30% reduction** in AWS monthly billing and a clean **$180K in annual cost savings**.