Micro Frontends: How to Scale Frontend Applications With Modular Architecture
Hello HaWkers, if you work at a company with large frontend teams or in a monolith that's hard to maintain, you've probably heard of micro frontends. This architecture promises to bring to the frontend the same benefits that microservices brought to the backend.
But is it worth it? And how do you implement it in a way that doesn't become a nightmare? Let's explore this in depth.
What Are Micro Frontends
Micro frontends is an architecture where a web application is divided into smaller, independent parts, each developed, tested, and deployed separately.
Simple analogy:
- Frontend monolith = A store where all departments share the same checkout
- Micro frontends = A mall where each store operates independently
Main characteristics:
- Deploy independence - Each part can be updated without affecting others
- Team autonomy - Different teams can work on different parts
- Technology agnostic - Each part can use different technologies
- Fault isolation - Problems in one part don't bring down the entire system
🏗️ Context: The term was coined in 2016, but gained traction from 2019 when companies like IKEA, Spotify and DAZN shared their experiences.
When to Use Micro Frontends
Before adopting this architecture, you need to be sure it solves a real problem:
Signs You Need It
Organizational indicators:
- Multiple teams working on the same frontend
- Constant merge conflicts
- Deploy blocked waiting for other features
- Very long build/test time
- Difficulty onboarding new devs
Technical indicators:
- Application with hundreds of thousands of lines of code
- Very large bundle size
- Degraded development performance
- Tests too slow
Signs You DON'T Need It
When to avoid:
- Small team (less than 10 frontend developers)
- Medium-sized application
- Strongly coupled business domain
- Early-stage startup
- Team without experience in distributed architecture
Rule of thumb: If you don't have clear scale or organization problems, micro frontends will add complexity without benefit.
Implementation Patterns
There are several ways to implement micro frontends. Let's look at the main ones:
1. Module Federation (Webpack 5+)
The most modern and popular approach currently:
// webpack.config.js for Host (Shell)
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
// Loads cart micro frontend from another server
cartMfe: 'cartMfe@http://localhost:3001/remoteEntry.js',
// Loads products micro frontend
productsMfe: 'productsMfe@http://localhost:3002/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
// webpack.config.js for Cart Micro Frontend
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'cartMfe',
filename: 'remoteEntry.js',
exposes: {
'./Cart': './src/components/Cart',
'./CartButton': './src/components/CartButton',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};Usage in Host:
// App.jsx in Host
import React, { Suspense } from 'react';
// Dynamic import of remote micro frontend
const RemoteCart = React.lazy(() => import('cartMfe/Cart'));
const RemoteProducts = React.lazy(() => import('productsMfe/ProductList'));
function App() {
return (
<div className="app">
<header>My Store</header>
<main>
<Suspense fallback={<div>Loading products...</div>}>
<RemoteProducts />
</Suspense>
</main>
<aside>
<Suspense fallback={<div>Loading cart...</div>}>
<RemoteCart />
</Suspense>
</aside>
</div>
);
}2. Single-SPA
Dedicated framework for orchestrating micro frontends:
// root-config.js
import { registerApplication, start } from 'single-spa';
// Register navigation micro frontend
registerApplication({
name: '@myorg/navbar',
app: () => System.import('@myorg/navbar'),
activeWhen: ['/'],
});
// Register products micro frontend (active on /products)
registerApplication({
name: '@myorg/products',
app: () => System.import('@myorg/products'),
activeWhen: ['/products'],
});
// Register checkout micro frontend (active on /checkout)
registerApplication({
name: '@myorg/checkout',
app: () => System.import('@myorg/checkout'),
activeWhen: ['/checkout'],
});
start();3. iFrames (Simple Approach)
For total isolation when necessary:
// Wrapper component for micro frontend via iframe
function MicroFrontendFrame({ src, title, height = '600px' }) {
return (
<iframe
src={src}
title={title}
style={{
width: '100%',
height,
border: 'none',
}}
sandbox="allow-scripts allow-same-origin allow-forms"
/>
);
}
// Usage
<MicroFrontendFrame
src="https://checkout.myapp.com"
title="Checkout"
height="800px"
/>;
Communication Between Micro Frontends
One of the biggest challenges is making micro frontends communicate:
Event Bus
Simple and effective pattern:
// eventBus.js - Shared between all MFEs
class EventBus {
constructor() {
this.events = {};
}
subscribe(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
// Returns unsubscribe function
return () => {
this.events[event] = this.events[event].filter((cb) => cb !== callback);
};
}
publish(event, data) {
if (this.events[event]) {
this.events[event].forEach((callback) => callback(data));
}
}
}
// Global singleton
window.eventBus = window.eventBus || new EventBus();
export default window.eventBus;Usage:
// In Products MFE
import eventBus from '@shared/eventBus';
function ProductCard({ product }) {
const addToCart = () => {
eventBus.publish('cart:add', {
productId: product.id,
quantity: 1,
});
};
return (
<div className="product">
<h3>{product.name}</h3>
<button onClick={addToCart}>Add to Cart</button>
</div>
);
}
// In Cart MFE
import eventBus from '@shared/eventBus';
function Cart() {
const [items, setItems] = useState([]);
useEffect(() => {
const unsubscribe = eventBus.subscribe('cart:add', (data) => {
setItems((prev) => [...prev, data]);
});
return unsubscribe;
}, []);
return <div>{/* Render items */}</div>;
}Custom Events (Browser Native)
Alternative without dependencies:
// Publish
window.dispatchEvent(
new CustomEvent('cart:updated', {
detail: { items: cartItems },
})
);
// Subscribe
window.addEventListener('cart:updated', (event) => {
console.log('Cart updated:', event.detail.items);
});
Challenges and Pitfalls
Micro frontends bring complexity. Be prepared:
1. UI Consistency
Problem: Each MFE can have different styles.
Solution: Shared Design System
// @myorg/design-system - Internal npm package
export { Button } from './components/Button';
export { Card } from './components/Card';
export { theme } from './theme';
export { GlobalStyles } from './GlobalStyles';2. Dependency Duplication
Problem: React loaded multiple times.
Solution: Shared dependencies in Module Federation
shared: {
react: {
singleton: true,
requiredVersion: '^18.0.0',
eager: true
}
}3. Performance
Problem: Multiple network requests.
Solutions:
- Prefetch critical MFEs
- Service workers for cache
- Shared CDN
4. End-to-End Testing
Problem: How to test the complete application?
Solution: Dedicated integration environment
# docker-compose.integration.yml
services:
shell:
build: ./packages/shell
ports:
- '3000:3000'
cart-mfe:
build: ./packages/cart
ports:
- '3001:3001'
products-mfe:
build: ./packages/products
ports:
- '3002:3002'
Ecosystem Tools
The ecosystem has evolved significantly in 2024-2025:
Build and Bundling
| Tool | Use | MFE Support |
|---|---|---|
| Webpack 5 | Native Module Federation | Excellent |
| Vite | vite-plugin-federation | Good |
| Rspack | Module Federation | Excellent |
| esbuild | Customization needed | Basic |
Orchestration Frameworks
| Framework | Complexity | Maturity |
|---|---|---|
| Single-SPA | High | High |
| Module Federation | Medium | High |
| Qiankun | Medium | Medium |
| Luigi (SAP) | High | High |
Monorepo Tools
For managing multiple MFEs:
- Nx - Most popular, great MFE support
- Turborepo - Performance, Vercel integration
- Lerna - Classic, fewer features
- pnpm workspaces - Lightweight and efficient
Conclusion
Micro frontends are a powerful tool for scaling large teams and applications. But like all distributed architecture, they bring complexity that needs to be justified.
Use if:
- Have multiple independent teams
- Genuinely large application
- Clear scale problems
- Experienced team in distributed systems
Avoid if:
- Small or medium team
- Normal-sized application
- Seeking solution for organizational problems
- No experience in distributed architecture
If you decide to implement, start small: extract one MFE, learn from the challenges, and scale gradually. The worst thing you can do is migrate everything at once.
To complement your knowledge in modern frontend architectures, I recommend checking out the article Vite vs Webpack in 2025 where you'll understand the build tools that support these architectures.
Let's go! 🦅
💻 Master JavaScript for Real
The knowledge you gained in this article is just the beginning. There are techniques, patterns, and practices that transform beginner developers into sought-after professionals.
Invest in Your Future
I've prepared complete material for you to master JavaScript:
Payment options:
- 1x of $4.90 no interest
- or $4.90 at sight

