Architecture

Micro-Frontend Architecture: Breaking the Monolith

10 min read By Suraj Kumar Gorai

Micro-frontends extend the microservices philosophy to the frontend: independently deployed, loosely coupled UI modules composed into a unified user experience. Having implemented this pattern for two enterprise platforms, here's what works, what hurts, and when to use it.

When Micro-Frontends Make Sense

Micro-frontends are an organizational architecture, not a technical one. They make sense when:

  • Multiple autonomous teams contribute to the same product but need independent release cycles
  • Different technology stacks coexist — legacy jQuery module alongside modern React features
  • Deploy independence is critical — Team A shipping shouldn't be blocked by Team B's test failures
  • Scale demands it — 50+ frontend developers on a single SPA becomes a merge conflict nightmare

If you have a small team (under 10 developers) and a single tech stack, a well-structured monolith is almost always better. Micro-frontends add complexity; justify that cost first.

Composition Strategies

1. Webpack Module Federation

The most popular approach for React/Vue/Angular apps. Each micro-frontend is a separate webpack build that exposes components as federated modules. The shell app imports them at runtime.

  • Pros: Shared dependencies (React loaded once), TypeScript types can be shared, feels like a monolith to users
  • Cons: Tightly coupled to webpack, shared dependency version conflicts are painful, debugging across module boundaries is complex

2. Route-Level Composition

Each micro-frontend owns entire routes. The shell handles routing and loads the appropriate app. Simplest to implement and reason about.

  • Pros: Clean boundaries, each team owns full pages, technology agnostic
  • Cons: No shared components within a page, navigation between apps can feel disjointed, duplicated common UI (headers, sidebars)

3. Web Components

Micro-frontends packaged as custom elements. Framework-agnostic by nature — a React component, Angular module, or vanilla JS widget all look the same to the shell.

  • Pros: True technology independence, browser-native API, encapsulated styles via Shadow DOM
  • Cons: Server-side rendering is difficult, Shadow DOM styling limitations, React/Vue wrappers add overhead

Shared State & Communication

Micro-frontends should be as independent as possible, but some state must be shared. Patterns that work:

  • Custom Events: Browser-native CustomEvent for loose coupling. One module dispatches, any module can listen. No shared runtime needed.
  • URL as state: Query parameters and path segments are the most universal state container. Every micro-frontend can read the URL.
  • Shared state bus: A lightweight event emitter or pub/sub library injected by the shell. Use sparingly — the more shared state, the more coupling.
  • Avoid: Shared Redux stores, shared React Context, or any pattern that creates runtime dependencies between micro-frontends.

Shared Design System

The biggest UX risk of micro-frontends is visual inconsistency. Mitigate with:

  • A versioned component library published to your internal npm registry
  • Design tokens (CSS custom properties) shared at the shell level
  • Visual regression tests that run across all micro-frontends
  • A dedicated design system team that maintains the shared library

Deployment Architecture

Each micro-frontend needs its own CI/CD pipeline deploying to its own origin (S3 bucket, CDN namespace, etc.). The shell app maintains a manifest mapping routes to micro-frontend bundle URLs.

  • Versioned manifests allow rolling back individual micro-frontends without redeploying the shell
  • Canary deployments per micro-frontend — route 5% of traffic to a new version, monitor, then promote
  • CDN caching with content-hashed filenames for immutable deployments

Performance Considerations

  • Shared dependency deduplication: React, React DOM, and your design system should load once, not per micro-frontend. Module Federation's shared config handles this.
  • Loading strategies: Lazy load micro-frontends on route entry. Show skeleton screens during the load. Prefetch likely next routes on hover.
  • Bundle size budgets: Enforce per-team bundle size limits in CI. Without limits, each team's "small addition" compounds into massive total bundles.

Lessons Learned

After two large-scale implementations:

  • Start with a monolith, extract micro-frontends when team scale demands it
  • Invest heavily in the shell app and shared tooling — they're the foundation everything depends on
  • Test the integration, not just the parts — E2E tests that span multiple micro-frontends catch the bugs unit tests miss
  • Document the architecture decisions and module boundaries — new developers need to understand the "why" behind the complexity

Considering micro-frontends for your platform? Let's evaluate whether it's the right fit for your team and product.