Micro-Frontend Architecture: Breaking the Monolith
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
CustomEventfor 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
sharedconfig 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.