How we migrated a monolithic React app serving 100M+ users to independently deployable micro-frontends — without a single minute of downtime, using the Strangler Fig pattern.
By mid-2024, our QuickBooks frontend was a 4-year-old React monolith with 380+ components, 12 product teams committing to the same repo, and a build time of 22 minutes. A single bad merge could block all 12 teams from deploying. CI became our biggest bottleneck — not our engineers.
We evaluated three approaches before committing: iframes (too isolated, poor UX), npm packages (too coupled, slow propagation across teams), and Webpack 5 Module Federation — the clear winner. Module Federation lets each MFE expose components that the Shell App loads at runtime, meaning no redeployment of the shell is needed when a child MFE ships a fix.
// shell/webpack.config.js — Host application
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
billing: 'billing@https://mfe-billing.intuit.com/remoteEntry.js',
reports: 'reports@https://mfe-reports.intuit.com/remoteEntry.js',
tax: 'tax@https://mfe-tax.intuit.com/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^19.0.0' },
'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
},
}),
],
};
// billing/webpack.config.js — Remote MFE
new ModuleFederationPlugin({
name: 'billing',
filename: 'remoteEntry.js',
exposes: {
'./BillingDashboard': './src/BillingDashboard',
'./InvoiceList': './src/InvoiceList',
},
shared: { react: { singleton: true } },
});
@intuit/shared-ui to internal npm registry. Built a lightweight event bus for cross-MFE communication to avoid prop-drilling across boundaries.// shared/event-bus.ts — Lightweight cross-MFE events
export const eventBus = {
emit: (event: string, data: unknown) =>
window.dispatchEvent(
new CustomEvent(event, { detail: data, bubbles: true })
),
on: (event: string, cb: (data: unknown) => void) => {
const handler = (e: Event) => cb((e as CustomEvent).detail);
window.addEventListener(event, handler);
return () => window.removeEventListener(event, handler);
},
};
// Usage: Billing MFE notifies Shell of payment success
eventBus.emit('payment:success', { invoiceId: 'INV-001', amount: 1200 });
// Shell listens and updates global nav badge
eventBus.on('payment:success', (data) => updateNavBadge(data));
singleton: true with requiredVersion in Module Federation config.webpack-bundle-analyzer.