Micro-Frontends Architecture: Patterns & Best Practices
Master micro-frontends architecture with proven patterns, integration strategies, and expert best practices. Build scalable frontend systems that grow with your team.
Micro-Frontends Architecture: Patterns and Best Practices
As modern web applications grow in complexity, the monolithic frontend has become one of the most persistent bottlenecks in enterprise software delivery. Teams working on large-scale platforms often find themselves fighting merge conflicts, navigating fragile shared codebases, and waiting in deployment queues that stretch for hours. Micro-frontends architecture emerges as the answer to these challenges — borrowing the modularity and team autonomy principles of microservices and applying them directly to the frontend layer. For senior engineers and architects evaluating how to scale both their applications and their teams, understanding this paradigm is no longer optional.
At its core, micro-frontends architecture is the practice of decomposing a frontend application into smaller, independently deployable units, each owned by a dedicated team. Each micro-frontend represents a distinct business domain — say, checkout, product catalog, or user profile — and can be built, tested, and released without coordinating with every other team. This organizational alignment between code ownership and business capability is what makes the pattern so compelling at scale. Rather than a purely technical decision, adopting micro-frontends is fundamentally an architectural and organizational strategy.
This guide dives deep into the integration patterns, composition strategies, communication mechanisms, and performance considerations that define production-grade micro-frontends architecture. Whether you are evaluating the pattern for the first time or refining an existing implementation, the insights here will help you build systems that are resilient, maintainable, and truly scalable.
Understanding the Core Principles of Micro-Frontends Architecture
Before selecting an integration pattern, it is essential to internalize the foundational principles that make micro-frontends architecture work in practice. The first principle is independent deployability: each micro-frontend must be deployable on its own without requiring a coordinated release with sibling applications. This means each unit ships with its own CI/CD pipeline, its own versioning strategy, and its own runtime dependencies. Violating this principle — even subtly, by sharing a global state store across boundaries — erodes the autonomy that justifies the architecture in the first place.
The second principle is technology agnosticism. A well-designed micro-frontends architecture allows different teams to choose the frontend framework that best suits their domain and skill set. In practice, most organizations converge on a shared framework (typically React or Angular) to reduce cognitive overhead, but the architecture itself should not enforce this constraint. The third principle is isolated failure: a runtime error in the recommendations widget should never crash the entire product page. Fault isolation through proper error boundaries and container fallback mechanisms is a non-negotiable requirement in any production deployment.
Integration Patterns: Choosing the Right Approach
The most consequential architectural decision in a micro-frontends project is how individual fragments are composed into a coherent user experience. There are three primary integration strategies, each with distinct trade-offs.
Build-Time Integration
In build-time integration, each micro-frontend is published as an npm package and imported by a container application that assembles the final bundle. This approach is straightforward to implement and familiar to any JavaScript developer, but it fundamentally undermines independent deployability. Every time a micro-frontend changes, the container must be rebuilt and redeployed. For teams with infrequent releases or strict governance requirements, this trade-off may be acceptable, but for high-velocity teams it quickly becomes a bottleneck. Build-time integration is best reserved for shared component libraries rather than full-feature micro-frontends.
Server-Side Composition
Server-side composition, often implemented via Edge Side Includes (ESI) or a dedicated composition layer such as Tailor or Mosaic, assembles the page on the server before delivering it to the browser. This approach yields excellent initial page load performance and strong SEO characteristics because the browser receives fully rendered HTML. The composition layer acts as a reverse proxy, fetching HTML fragments from each micro-frontend service and stitching them together based on a page template. Netflix and Zalando have popularized this pattern at scale, demonstrating that it can handle tens of thousands of requests per second when coupled with aggressive fragment caching strategies.
Client-Side Composition with Module Federation
Client-side composition is currently the dominant pattern for single-page application ecosystems, and Webpack 5's Module Federation plugin has dramatically simplified its implementation. With Module Federation, each micro-frontend exposes a set of JavaScript modules at runtime, and a shell application dynamically imports them as needed. The following configuration illustrates a basic setup:
// webpack.config.js — Remote (Product micro-frontend)
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'productApp',
filename: 'remoteEntry.js',
exposes: {
'./ProductCatalog': './src/ProductCatalog',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),
],
};
// webpack.config.js — Host (Shell application)
new ModuleFederationPlugin({
name: 'shell',
remotes: {
productApp: 'productApp@https://products.example.com/remoteEntry.js',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
});
The singleton: true flag for React is critical — it ensures only one instance of the framework runs in the browser, preventing the notorious "multiple React instances" error that plagued earlier iframe-based approaches. Module Federation also supports version negotiation, meaning if the shell requires React 18 and a remote ships React 17, Webpack will attempt to resolve a compatible shared version at runtime.
Communication Between Micro-Frontends
Once micro-frontends are composed on a page, they inevitably need to exchange information. The communication mechanism you choose has a direct impact on coupling levels and long-term maintainability. There are three well-established patterns, and the right choice depends on the nature of the interaction.
Custom Events and the Browser Event Bus
The most decoupled communication mechanism available in the browser is the native CustomEvent API. A micro-frontend dispatches an event on window, and any other micro-frontend that has registered a listener receives it. This approach introduces zero shared dependencies between micro-frontends, which preserves their independence entirely. The downside is the lack of a typed contract — teams must agree on event schemas out-of-band, often through a shared API contract document or an AsyncAPI specification.
// Dispatching from the Cart micro-frontend
window.dispatchEvent(new CustomEvent('cart:itemAdded', {
detail: { productId: 'SKU-42', quantity: 1 }
}));
// Listening in the Header micro-frontend
window.addEventListener('cart:itemAdded', (event) => {
updateCartBadge(event.detail.quantity);
});
Shared State with Props and Callbacks
When two micro-frontends are rendered by the same shell application, the shell can pass data down as props and receive updates via callbacks — a pattern identical to standard React component communication. This approach is strongly typed and easy to test, but it requires the shell to act as an intermediary, which introduces orchestration logic that can grow unwieldy. Reserve this pattern for tightly related fragments where the shell naturally owns the shared state.
URL and Query String as State
For navigation-level state — filters, selected tabs, active entity IDs — the URL is the most durable and shareable state container available. Multiple micro-frontends can read from and write to the URL without direct coupling, and the user benefits from bookmarkable, shareable application states. Establishing clear URL ownership conventions (which micro-frontend owns which query parameters) prevents collisions and keeps the coordination surface minimal.
Performance Optimization in Micro-Frontends Architecture
One of the most frequently cited concerns about micro-frontends architecture is performance degradation caused by redundant code loading. Without deliberate optimization, multiple micro-frontends will each ship their own copy of React, lodash, and other common libraries, inflating the page's JavaScript payload significantly.
Shared Dependencies and Versioning Strategy
Module Federation's shared scope mechanism is the primary tool for eliminating duplicate dependencies in client-side compositions. By declaring a library as shared, Webpack ensures only one copy is loaded regardless of how many remotes reference it. The challenge arises when remotes ship incompatible versions of a shared library — a scenario best mitigated through a formal dependency governance policy. Many teams adopt a "platform team" model, where a central team owns the canonical versions of shared dependencies and publishes them on a defined upgrade cadence.
Lazy Loading and Route-Based Code Splitting
Each micro-frontend should only load when the user navigates to the relevant section of the application. React's lazy and Suspense APIs, combined with Module Federation's dynamic import capability, make this straightforward to implement. A product details page should never load the checkout micro-frontend's JavaScript until the user initiates a purchase flow. This discipline keeps Time to Interactive (TTI) metrics competitive with traditional SPAs.
Edge Caching for Server-Side Composed Fragments
In server-side composition architectures, fragment-level caching at the CDN edge is the highest-leverage performance optimization available. By assigning appropriate Cache-Control headers to each fragment — with cache lifetimes tuned to how frequently the fragment's content changes — a composition layer can serve the majority of page requests from cache without hitting origin services. The header fragment showing the user's cart count, being highly personalized, might have a TTL of zero, while the static navigation fragment could be cached for hours.
Testing Strategy for Micro-Frontends
Testing micro-frontends requires a layered strategy that balances confidence with execution speed. Unit tests within each micro-frontend validate component logic in isolation and should form the bulk of the test suite. Contract tests — using tools like Pact — verify that the communication contracts between micro-frontends remain stable as each team deploys independently. Finally, end-to-end tests executed against a fully composed environment validate critical user journeys that span multiple micro-frontend boundaries, such as adding an item to the cart and completing checkout.
One pattern that significantly reduces the cost of integration testing is the test harness shell — a lightweight container application maintained by each team specifically for running their micro-frontend in an integrated context. Rather than spinning up the full production shell for every integration test, the test harness shell provides just enough scaffolding to validate cross-boundary interactions. This approach keeps test execution fast and reduces environment dependencies during CI runs.
Organizational Alignment: Conway's Law in Practice
Micro-frontends architecture does not exist in a vacuum — it is deeply entangled with how engineering teams are structured. Conway's Law states that organizations design systems that mirror their communication structures, and this principle applies with particular force to micro-frontends. Teams that own a micro-frontend end-to-end — from design and backend API to frontend rendering and deployment — ship faster and produce more cohesive user experiences than teams organized around technical layers.
Establishing clear domain boundaries before writing a single line of code is therefore the most important prerequisite for a successful micro-frontends adoption. Domain-driven design (DDD) bounded contexts provide an excellent framework for this exercise. When the frontend domain boundaries align with the service boundaries in the backend, the architecture achieves a natural coherence that reduces the need for cross-team coordination on a day-to-day basis.
Conclusion: Building for Scale with Micro-Frontends Architecture
Micro-frontends architecture represents a mature, battle-tested approach to scaling frontend development across large teams and complex business domains. By embracing independent deployability, fault isolation, and domain ownership, organizations can dramatically reduce the coordination overhead that strangles productivity in monolithic frontend systems. The patterns explored here — from Module Federation-based client-side composition to server-side fragment assembly and event-driven communication — provide a comprehensive toolkit for building production-grade systems.
The path to a successful micro-frontends implementation, however, demands more than technical knowledge. It requires deliberate organizational design, rigorous governance of shared dependencies, and a testing strategy that builds confidence without sacrificing deployment velocity. Teams that invest in these foundations consistently outperform those that treat micro-frontends architecture as a purely technical refactoring exercise.
At Nordiso, our architects have guided enterprise clients through every phase of micro-frontends adoption — from initial domain decomposition and technology selection to production rollout and performance optimization. If your team is evaluating whether micro-frontends architecture is the right path forward, or if you are already mid-migration and need expert guidance, we would be glad to help you build something exceptional.

