Writing About Contact
Engineering 15 min read

Micro-Frontends: An 18-Month Post-Mortem

We broke a monolith into 6 micro-frontends. Here's what the architecture docs didn't tell us about module federation, shared state, and the real cost of team autonomy.

Honey Sharma

Eighteen months ago, we decided to break our React monolith into six micro-frontends using Webpack Module Federation. The decision was well-reasoned at the time: four separate teams, conflicting release schedules, a design system that was growing faster than any single team could manage.

This is what actually happened.

The Pitch vs. The Reality

The pitch: Teams deploy independently. No more merge conflicts across team boundaries. Each micro-frontend can use different React versions if needed. The design system becomes a separately deployed package.

The reality: We spent the first six months solving problems that didn’t exist in the monolith.

Module Federation sounds elegant in the docs. In practice:

// Host app (shell)
new ModuleFederationPlugin({
  name: 'shell',
  remotes: {
    checkout: 'checkout@http://localhost:3001/remoteEntry.js',
    catalog: 'catalog@http://localhost:3002/remoteEntry.js',
  },
  shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
})

// Checkout app
new ModuleFederationPlugin({
  name: 'checkout',
  filename: 'remoteEntry.js',
  exposes: { './CheckoutFlow': './src/CheckoutFlow' },
  shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
})

The shared config is where you cry. If two apps accidentally ship different patch versions of a shared dependency, you get silent duplicate instances. React Context breaks across the boundary. Global event handlers multiply. We had a three-day incident caused by a minor version bump in date-fns.

Shared State: The Unsolvable Problem

In a monolith, sharing state is trivial. In micro-frontends, it’s a philosophical debate.

The options we evaluated:

  1. Custom Events — works, but you’ve invented a message bus with no types
  2. Shared Redux store — couples all apps to a central store, defeats the purpose
  3. URL as state — correct for cross-app navigation state, but awkward for ephemeral UI state
  4. BroadcastChannel API — tab-scoped, surprisingly useful for user session state

We ended up with a hybrid: URL for navigation state (page, filters, selected items), BroadcastChannel for session state (auth token refresh, user preferences), and absolutely nothing shared for UI state within a micro-frontend boundary.

The Testing Pyramid Inverted

In the monolith, unit tests were fast and integration tests caught real bugs. With micro-frontends, integration tests became the only thing that caught real bugs — because the bugs lived at the boundaries.

We needed full Playwright tests that booted all six apps simultaneously to catch integration regressions. Our CI pipeline went from 4 minutes to 22 minutes.

What Actually Worked

Team autonomy for deployments: The checkout team could ship hotfixes without coordinating with anyone. This was real and valuable.

Design system isolation: The shared component library became a first-class product with its own semantic versioning. Breaking changes were explicit and trackable.

Independent scaling: During flash sale events, we could scale the catalog service independently of the checkout service. Not a huge win for frontend, but it aligned with the backend team’s mental model.

The Honest Assessment

If your main problems are: team coordination on deployment schedules, and different teams needing to move at different speeds — micro-frontends solve that.

If your main problems are: performance, complexity management, or developer experience — micro-frontends make all of those significantly worse.

The architecture that would have served us better: a monorepo with shared packages, separate CI/CD pipelines per team directory, and clear module boundaries enforced by ESLint. Same deployment flexibility, a fraction of the operational complexity.

We’re not migrating back. The switching cost is too high. But if I’m starting fresh, I’m not starting with module federation.

Related Reading