Micro-Frontends Architecture: Patterns and Best Practices
Explore cutting-edge micro-frontends architecture patterns and best practices for senior developers. Learn composition, routing, and integration strategies from Nordiso’s Finnish engineering team.
Introduction
As frontend applications grow beyond the capacity of a single team, the limitations of monolithic SPAs become painfully clear. Scaling development velocity, independent deployments, and team autonomy demands a fundamental shift in how we structure user interfaces. This is precisely where micro-frontends architecture enters the picture — not as a silver bullet, but as a proven architectural paradigm that mirrors the backend advantages of microservices at the UI layer.
By decomposing a frontend into smaller, loosely coupled pieces, each owned by a dedicated team, you gain the ability to deploy, test, and scale independently. However, this decomposition introduces new challenges: shared dependency management, consistent user experience, inter-application communication, and performance overhead. In this technical deep dive, we unpack the most effective patterns and practices for implementing micro-frontends architecture in production, drawing on real-world experience from Nordiso’s work with enterprise clients across the Nordics.
Whether you are evaluating a proof of concept or refactoring a tangled monolith, this guide will help you navigate the trade-offs and choose the right composition strategy for your organization.
The Core Problem: Why Micro-Frontends Architecture Matters
Modern SaaS products often require multiple teams to contribute to a single UI. A typical enterprise platform might have a payments team, a user management team, and a reporting team — all working on the same application. Without micro-frontends, each team’s code is tightly coupled, resulting in merge conflicts, long CI pipelines, and cascading failures. Micro-frontends architecture solves this by decoupling the frontend into independent modules that can be developed, tested, and deployed in isolation.
Yet, the transition from monolith to micro-frontends is not merely a technological shift; it is an organizational one. Conway’s Law applies with a vengeance — your UI structure must mirror your team structure. When done right, this architecture enables faster feature delivery and reduces coordination overhead. When done poorly, it introduces duplication, inconsistent styling, and a fragmented user journey.
Composition Patterns: How to Assemble Micro-Frontends
Server-Side Composition
Server-side composition involves assembling micro-frontends on the backend before delivering the HTML to the browser. Each micro-frontend is rendered as a fragment on the server, and the final page is stitched together — often using Edge Side Includes (ESI) or a bespoke templating engine. This pattern is ideal for SEO-critical pages where initial load performance is paramount.
For example, using an Nginx server with SSI (Server Side Includes), you can compose fragments from different upstream services:
<!-- index.html -->
<html>
<body>
<header>
<!--# include virtual="/header/header.html" -->
</header>
<main>
<!--# include virtual="/products/product-list.html" -->
</main>
<footer>
<!--# include virtual="/footer/footer.html" -->
</footer>
</body>
</html>
This approach keeps the client lightweight and avoids JavaScript runtime overhead, but it requires careful caching strategies and infrastructure to manage latency across multiple backend services.
Client-Side Composition (Runtime Integration)
Client-side composition relies on JavaScript to dynamically fetch and render micro-frontends in the browser. Web Components are a popular choice for this pattern due to their encapsulation and framework-agnostic nature. Each micro-frontend is deployed as a standalone bundle and loaded on demand via a module federation plugin, such as Webpack 5’s ModuleFederationPlugin.
Consider a shell application that orchestrates three independent micro-frontends:
// shell-app/webpack.config.js
new ModuleFederationPlugin({
name: 'shell',
remotes: {
header: 'header@http://localhost:3001/remoteEntry.js',
dashboard: 'dashboard@http://localhost:3002/remoteEntry.js',
settings: 'settings@http://localhost:3003/remoteEntry.js'
},
shared: ['react', 'react-dom']
})
This pattern excels in teams that need independent deployment cycles. However, it introduces a shared vendor chunk challenge: if not handled carefully, users may download duplicate dependencies, inflating bundle size. Using singleton: true in shared configurations mitigates this risk.
Iframe-Based Composition
Iframes offer the strongest isolation — each micro-frontend runs in its own document context with independent JavaScript execution and CSS scoping. This is useful for legacy or high-security domains (e.g., payment widgets). The trade-off is poor SEO, accessibility, and inter-window communication overhead.
Best practice: Avoid iframes for core navigation flows. Reserve them for third-party integrations or sandboxed widgets where security outweighs UX concerns.
Routing and Navigation Best Practices
Centralized Routing with a Shell
The shell application (or host) owns the top-level router. When a user navigates to a route, the shell decides whether to render the micro-frontend locally or fetch it from a remote. This prevents routing conflicts and ensures a single source of truth for the URL structure.
// Shell router (React Router)
<Routes>
<Route path="/dashboard/*" element={<DashboardWrapper />} />
<Route path="/settings/*" element={<SettingsWrapper />} />
</Routes>
Each wrapper component loads the corresponding remote micro-frontend and passes navigation context (e.g., current user, tenant ID).
Decentralized Routing (for Sub-Apps)
Some architectures allow each micro-frontend to own its internal routing, with the shell only forwarding a base path. This works well when sub-apps are truly independent products (e.g., a CRM module vs. an admin panel). The shell must communicate route changes back to the micro-frontend via custom events or a shared state store.
Communication Patterns Between Micro-Frontends
Custom Events (The Lightweight Choice)
The simplest approach is to use the browser’s native CustomEvent API. One micro-frontend dispatches an event on window, and others listen for it. This is ideal for cross-cutting concerns like user logout or theme changes.
// Header micro-frontend
dispatchEvent(new CustomEvent('user:logout', { detail: { userId } }));
// Dashboard micro-frontend
addEventListener('user:logout', (e) => {
clearUserCache(e.detail.userId);
});
Drawback: No type safety or contract enforcement. Use a shared event contract library to mitigate this.
Shared Reactive State (Global Store)
For complex state flows (e.g., a cart shared between product listing and checkout), a central store like Redux, Zustand, or a simple RxJS Subject can be shared via module federation. Both micro-frontends import the same store instance from a shared lib.
// shared-lib/store.js
import { createStore } from 'zustand/vanilla';
export const useCartStore = createStore((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] }))
}));
Caution: Avoid over-engineering. Shared state creates coupling — only share what is strictly necessary.
Formal Messaging Bus
For large-scale systems with many micro-frontends, a dedicated messaging layer (e.g., via PostMessage or a lightweight pub/sub library like Postal.js) provides structure. Define message schemas in a shared contract package, versioned alongside each micro-frontend.
Performance Optimization in Micro-Frontends Architecture
Lazy Loading and Code Splitting
Each micro-frontend should be loaded only when needed. Use dynamic imports in the shell to fetch the remote entry point on route change. Combine this with preloading hints (e.g., <link rel="preload">) for likely navigation paths.
Shared Dependencies (Singleton Strategy)
To avoid duplicate libraries, declare shared dependencies as singletons in your module federation config. For critical libraries like React or Angular, use singleton: true and requiredVersion to match versions across teams.
Caching HTTP Responses
Micro-frontends exposed as separate build artifacts should have aggressive HTTP caching (e.g., Cache-Control: public, max-age=31536000, immutable) combined with content-hashed filenames. This ensures that users download each micro-frontend only once per version.
Real-World Scenario: Nordiso’s Approach to Banking UI
Nordiso recently helped a Nordic bank migrate a monolithic online banking portal to a micro-frontends architecture. The bank had five teams: Accounts, Payments, Loans, Investments, and Settings. Each team owned a distinct domain.
We chose a hybrid composition pattern:
- Server-side composition for the login and onboarding flows (SEO-critical).
- Client-side composition via Webpack Module Federation for the authenticated dashboard.
- Custom event bus for cross-cutting concerns like user logout and fraud alerts.
The result: deployment frequency increased from once per week to multiple times per day, and a team’s bug fix no longer required a full regression test of the entire frontend.
Common Pitfalls to Avoid
- Too much duplication: Shared UI component libraries and design tokens reduce visual fragmentation.
- Monolithic shell: The shell should be thin — avoid adding business logic that creates a single point of failure.
- Ignoring testing: Each micro-frontend must have its own integration tests in isolation, plus cross-app E2E tests for critical flows.
- Neglecting observability: Log events, metrics, and errors from each micro-frontend with consistent correlation IDs.
Conclusion
Micro-frontends architecture is not a new trend — it is an evolutionary response to the complexity of modern web applications. When implemented with careful consideration of composition patterns, routing, communication, and performance, it empowers teams to build and scale independently without sacrificing user experience. The key is to avoid dogmatism: choose the integration strategy that matches your team topology and performance requirements.
At Nordiso, our senior engineers have deep experience designing and implementing scalable micro-frontends that align with business goals. If you are ready to decompose your monolithic frontend or need a second opinion on your architecture, we invite you to reach out for a consultation. Let’s build the next generation of your UI — decoupled, fast, and maintainable.

