Microservices transformed how backend systems are built by breaking monolithic applications into independently deployable services. Micro-frontends apply the same principle to the frontend — splitting a large web application into smaller, self-contained pieces that can be developed, tested, and deployed by independent teams.
At StrikingWeb, we have implemented micro-frontend architectures for enterprise clients where the scale of their frontend development — dozens of developers across multiple teams — made the monolithic single-page application approach unsustainable. The results have been transformative for development velocity, but the architecture introduces complexity that must be managed deliberately.
The Problem Micro-Frontends Solve
Consider a large e-commerce platform with teams responsible for product catalog, shopping cart, user accounts, checkout, and content management. In a monolithic frontend, all these concerns live in the same codebase. Changes to the cart component require coordination with the catalog team. A dependency upgrade affects everyone. Deployment requires the entire application to be built and released as a unit.
As the codebase grows and more teams contribute, these coordination costs escalate. Pull request reviews become bottlenecks. Merge conflicts multiply. A broken test in one area blocks deployments for every team. The frontend becomes the organizational constraint that microservices were supposed to eliminate on the backend.
Micro-frontends solve this by giving each team ownership of a vertical slice of the application — from the user interface to the API layer. Each team can choose their own technology stack, release on their own schedule, and operate independently.
Composition Patterns
The central architectural question in micro-frontends is how the pieces come together into a cohesive user experience. Several composition patterns exist, each with different tradeoffs.
Build-Time Composition
Micro-frontends are published as packages and assembled during the build process. The shell application imports each micro-frontend as a dependency and bundles them together. This is the simplest approach but sacrifices independent deployability — changes to any micro-frontend require rebuilding and redeploying the shell.
Server-Side Composition
A server-side orchestrator fetches HTML fragments from each micro-frontend service and assembles the page before sending it to the browser. This approach works well with server-side rendering and is used in platforms like Zalando's Project Mosaic and Podium. The trade-off is additional server-side infrastructure and latency from fragment fetching.
Client-Side Composition
Each micro-frontend is loaded as a separate JavaScript bundle in the browser. A shell application manages routing and mounting micro-frontends into designated DOM areas. This is the most common pattern and is well-supported by tools like single-spa and Webpack Module Federation.
Edge-Side Composition
CDN edge functions assemble pages from micro-frontend fragments. This combines the performance benefits of server-side composition with global distribution. Edge-side includes (ESI) have been used for this purpose for years, and modern edge computing platforms like Cloudflare Workers make more sophisticated compositions possible.
Webpack Module Federation
Module Federation, introduced in Webpack 5, has become the most popular technical foundation for micro-frontends. It allows multiple independently built applications to share code at runtime without bundling it together at build time.
How It Works
Each micro-frontend exposes specific modules — components, utilities, or entire feature sections — through its Webpack configuration. Other micro-frontends can import these exposed modules at runtime. Shared dependencies like React are loaded once and shared across all micro-frontends, avoiding duplicate code.
// product-catalog/webpack.config.js
new ModuleFederationPlugin({
name: 'productCatalog',
filename: 'remoteEntry.js',
exposes: {
'./ProductGrid': './src/components/ProductGrid',
'./ProductDetail': './src/components/ProductDetail',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
})
The shell application declares which remote modules it consumes and loads them dynamically. If the product catalog team deploys a new version of ProductGrid, the shell application picks up the change immediately without redeployment.
Shared Dependencies
Managing shared dependencies is one of the trickiest aspects of Module Federation. React, for example, must be a singleton — multiple instances cause hooks to break. The shared configuration ensures only one version is loaded, using semantic versioning to negotiate which version to use when micro-frontends specify different ranges.
Maintaining Consistency
The biggest risk with micro-frontends is a fragmented user experience. If each team makes independent design decisions, the application feels like a patchwork of different products rather than a unified platform.
Design System as a Shared Contract
A shared design system is essential for visual and interaction consistency across micro-frontends. This design system should be published as a package that every team imports, providing common components (buttons, forms, modals), a shared design token system (colors, spacing, typography), layout patterns and grid systems, and accessibility defaults built into every component.
The design system team serves as a platform team — building the foundation that product teams build upon. They do not own any specific feature but ensure that every feature looks and feels cohesive.
Cross-Cutting Concerns
Authentication, analytics, error tracking, and routing typically span multiple micro-frontends. These cross-cutting concerns belong in the shell application or in shared libraries:
- Authentication: The shell manages user sessions and passes authentication state to micro-frontends through a shared context or event bus
- Routing: The shell handles top-level navigation while each micro-frontend manages its internal routes
- Analytics: A shared analytics layer captures page views and events consistently across micro-frontends
- Error boundaries: Each micro-frontend should be wrapped in an error boundary so that a failure in one section does not crash the entire application
Communication Between Micro-Frontends
Micro-frontends sometimes need to communicate — the cart needs to know when a product is added from the catalog, the header needs to display the current cart count. This communication should be loosely coupled to maintain independence.
The golden rule of micro-frontend communication: prefer events over direct imports. A micro-frontend should never import code directly from another micro-frontend. Instead, use custom events, a shared event bus, or a lightweight state management layer in the shell.
Custom browser events are the simplest approach. The catalog dispatches a product-added-to-cart event with the product data. The cart listens for this event and updates its state. Neither team needs to know about the other's implementation.
Testing Strategies
Testing micro-frontends requires a layered approach. Each micro-frontend has its own unit and integration tests that run in isolation. End-to-end tests that verify the composed application run in a shared environment that mirrors production.
Contract testing is valuable for verifying that exposed modules and event interfaces remain compatible. If the catalog team changes the shape of the product-added-to-cart event, contract tests catch the incompatibility before it reaches production.
When Micro-Frontends Make Sense
Micro-frontends add significant architectural complexity. They are justified when the organization has multiple teams working on the same frontend and coordination costs are high. They are not justified for small teams or applications where a well-organized monolithic codebase serves the same purpose with less overhead.
Our guideline: if your frontend team is fewer than fifteen developers, a monolithic application with good module boundaries is likely more productive. Above that threshold — particularly when teams span different time zones, business units, or technology preferences — micro-frontends start paying for their complexity.
At StrikingWeb, we help organizations evaluate whether micro-frontends fit their situation and, when they do, implement them with the architectural rigor needed to avoid fragmentation. If you are experiencing growing pains with your frontend codebase, we would be glad to discuss whether micro-frontends are the right solution.