Microfrontends: The Architecture Big Companies Use to Scale Teams and Applications
Hello HaWkers, imagine having 50 developers working on the same frontend application without one team blocking another. Sounds impossible? Companies like Spotify, IKEA, Amazon, and Zalando do this every day using microfrontends - and this architecture is becoming increasingly relevant in 2025.
The problem that microfrontends solve is very real: as applications and teams grow, frontend monoliths become bottlenecks. Coordinated deployments between teams, endless merge conflicts, legacy technologies blocking innovation. Microfrontends bring to the frontend the same benefits that microservices brought to the backend.
What Are Microfrontends?
Microfrontends is an architecture that divides a frontend application into smaller, independent pieces, each of which can be developed, tested, and deployed by different teams using potentially different technologies.
Think of an e-commerce: the catalog team handles product listings, the checkout team handles cart and payment, the account team handles user profile. Each team works on its own "micro-app", but to the end user everything appears as a single, integrated application.
The big insight is autonomy. Each team can choose their stack (React, Vue, Angular), define their own release cycle, and make independent deployments without coordinating with 10 other teams.
// Conceptual example: Project structure with microfrontends
/*
my-ecommerce/
βββ container/ // Shell application that orchestrates everything
β βββ src/
β β βββ App.jsx
β β βββ bootstrap.jsx
β βββ webpack.config.js // Module Federation config
β βββ package.json
β
βββ products/ // Products microfrontend (team A)
β βββ src/
β β βββ ProductList.jsx
β β βββ ProductDetail.jsx
β β βββ index.js // Exports to expose
β βββ webpack.config.js
β βββ package.json // React 18 + their libs
β
βββ cart/ // Cart microfrontend (team B)
β βββ src/
β β βββ Cart.jsx
β β βββ Checkout.jsx
β β βββ index.js
β βββ webpack.config.js
β βββ package.json // Can even use Vue if they want!
β
βββ account/ // Account microfrontend (team C)
βββ src/
β βββ Profile.jsx
β βββ Orders.jsx
β βββ index.js
βββ webpack.config.js
βββ package.json // React 17 + specific libs
*/
// container/webpack.config.js - Orchestrator
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'container',
remotes: {
// Load microfrontends remotely
products: 'products@http://localhost:3001/remoteEntry.js',
cart: 'cart@http://localhost:3002/remoteEntry.js',
account: 'account@http://localhost:3003/remoteEntry.js'
},
shared: {
// Share dependencies to avoid duplication
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' }
}
})
]
};
// container/src/App.jsx - Microfrontend composition
import React, { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// Lazy load microfrontends
const ProductList = lazy(() => import('products/ProductList'));
const ProductDetail = lazy(() => import('products/ProductDetail'));
const Cart = lazy(() => import('cart/Cart'));
const Checkout = lazy(() => import('cart/Checkout'));
const Profile = lazy(() => import('account/Profile'));
const Orders = lazy(() => import('account/Orders'));
function App() {
return (
<BrowserRouter>
<div className="app">
<Header /> {/* Container component */}
<Suspense fallback={<div>Loading...</div>}>
<Routes>
{/* Routes managed by products */}
<Route path="/products" element={<ProductList />} />
<Route path="/products/:id" element={<ProductDetail />} />
{/* Routes managed by cart */}
<Route path="/cart" element={<Cart />} />
<Route path="/checkout" element={<Checkout />} />
{/* Routes managed by account */}
<Route path="/profile" element={<Profile />} />
<Route path="/orders" element={<Orders />} />
</Routes>
</Suspense>
<Footer /> {/* Container component */}
</div>
</BrowserRouter>
);
}
export default App;
Each microfrontend runs on its own development server and can be deployed independently. The container orchestrates them at runtime.
Module Federation: The Technology That Made Microfrontends Practical
Module Federation, introduced in Webpack 5, revolutionized microfrontends by enabling dynamic sharing of JavaScript code between completely separate applications.
Before Module Federation, implementing microfrontends was complex and full of trade-offs. Now it is surprisingly elegant.
// products/webpack.config.js - Microfrontend that exposes components
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const deps = require('./package.json').dependencies;
module.exports = {
entry: './src/index.js',
mode: 'development',
devServer: {
port: 3001,
headers: {
'Access-Control-Allow-Origin': '*'
}
},
plugins: [
new ModuleFederationPlugin({
name: 'products',
filename: 'remoteEntry.js',
// What this microfrontend exposes
exposes: {
'./ProductList': './src/components/ProductList',
'./ProductDetail': './src/components/ProductDetail',
'./ProductCard': './src/components/ProductCard'
},
// What it can consume from others
remotes: {
cart: 'cart@http://localhost:3002/remoteEntry.js'
},
// Shared dependencies
shared: {
...deps,
react: {
singleton: true,
requiredVersion: deps.react,
eager: false
},
'react-dom': {
singleton: true,
requiredVersion: deps['react-dom'],
eager: false
}
}
})
]
};
// products/src/components/ProductList.jsx
import React, { useState, useEffect } from 'react';
// Can import component from ANOTHER microfrontend!
import { AddToCartButton } from 'cart/AddToCartButton';
export default function ProductList() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(setProducts);
}, []);
return (
<div className="product-grid">
{products.map(product => (
<div key={product.id} className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p className="price">${product.price}</p>
{/* Using component from "cart" microfrontend */}
<AddToCartButton productId={product.id} />
</div>
))}
</div>
);
}
The magic is in how Module Federation manages shared dependencies. If products
and cart
use React 18, only one copy is loaded and shared between them.
Communication Patterns Between Microfrontends
The challenge in microfrontends is communication. How does the cart microfrontend know when a product was added? How to synchronize state between independent apps?
There are several patterns:
1. Custom Events (Browser API)
// products/ProductCard.jsx - Dispatches event
function addToCart(product) {
// Local logic
const event = new CustomEvent('product:added-to-cart', {
detail: { product }
});
window.dispatchEvent(event);
}
// cart/CartIcon.jsx - Listens to event
import { useEffect, useState } from 'react';
function CartIcon() {
const [itemCount, setItemCount] = useState(0);
useEffect(() => {
function handleProductAdded(event) {
console.log('Product added:', event.detail.product);
setItemCount(prev => prev + 1);
}
window.addEventListener('product:added-to-cart', handleProductAdded);
return () => {
window.removeEventListener('product:added-to-cart', handleProductAdded);
};
}, []);
return (
<div className="cart-icon">
π {itemCount > 0 && <span className="badge">{itemCount}</span>}
</div>
);
}
2. Shared State Store (Redux, Zustand, etc)
// shared/store.js - Store shared by all
import create from 'zustand';
export const useCartStore = create((set) => ({
items: [],
addItem: (product) => set((state) => ({
items: [...state.items, product]
})),
removeItem: (productId) => set((state) => ({
items: state.items.filter(item => item.id !== productId)
})),
getTotal: () => {
// Calculation logic
}
}));
// Exposed by container and shared
// container/webpack.config.js
new ModuleFederationPlugin({
name: 'container',
exposes: {
'./store': './src/shared/store'
},
// ...
});
// products/ProductCard.jsx - Uses shared store
import { useCartStore } from 'container/store';
function ProductCard({ product }) {
const addItem = useCartStore(state => state.addItem);
return (
<div>
<h3>{product.name}</h3>
<button onClick={() => addItem(product)}>
Add to Cart
</button>
</div>
);
}
// cart/Cart.jsx - Also uses the same store
import { useCartStore } from 'container/store';
function Cart() {
const items = useCartStore(state => state.items);
const removeItem = useCartStore(state => state.removeItem);
return (
<div>
<h2>Cart ({items.length})</h2>
{items.map(item => (
<div key={item.id}>
<span>{item.name}</span>
<button onClick={() => removeItem(item.id)}>Remove</button>
</div>
))}
</div>
);
}
3. Props Drilling (Container passes props)
// container/App.jsx - Container manages state and passes props
import React, { useState } from 'react';
import ProductList from 'products/ProductList';
import CartIcon from 'cart/CartIcon';
function App() {
const [cartItems, setCartItems] = useState([]);
const handleAddToCart = (product) => {
setCartItems(prev => [...prev, product]);
};
const handleRemoveFromCart = (productId) => {
setCartItems(prev => prev.filter(item => item.id !== productId));
};
return (
<div>
<header>
<CartIcon
items={cartItems}
onRemove={handleRemoveFromCart}
/>
</header>
<ProductList onAddToCart={handleAddToCart} />
</div>
);
}
Challenges and Trade-offs of Microfrontends
Microfrontends are not a magic solution. They bring complexity:
1. Dependency Duplication Even with shared modules, different library versions can be loaded, increasing the total bundle.
2. Deployment Complexity Now you have multiple deployments to coordinate. Versioning and compatibility between microfrontends requires discipline.
3. End-to-End Tests Testing the complete integration is more complex when parts run in different repositories and pipelines.
4. Performance Additional network requests to load remotes can impact initial loading time.
// Strategy to minimize performance issues
// container/webpack.config.js
new ModuleFederationPlugin({
name: 'container',
remotes: {
// In production, use CDN with specific version
products: process.env.NODE_ENV === 'production'
? 'products@https://cdn.example.com/products/v1.2.3/remoteEntry.js'
: 'products@http://localhost:3001/remoteEntry.js',
},
shared: {
react: {
singleton: true,
eager: true, // Load immediately in container
requiredVersion: '^18.0.0'
}
}
});
// Prefetch strategy to improve perceived performance
// container/src/App.jsx
import { useEffect } from 'react';
function App() {
useEffect(() => {
// Prefetch microfrontends that will likely be used
import('products/ProductList');
import('cart/CartIcon');
}, []);
return (
// ...
);
}
When to Use (And When NOT to Use) Microfrontends
Use microfrontends when:
- You have multiple teams working on the same product
- Parts of the application evolve at very different paces
- You need to gradually migrate from one technology to another
- Different parts have very different scale requirements
DO NOT use microfrontends when:
- Your team has less than 10 developers
- Your application is relatively simple
- You do not have robust CI/CD infrastructure
- Performance is critical and you cannot accept overhead
For most small and medium projects, a well-structured monolith is simpler and more effective.
The Future of Microfrontends
The trend is strong. Tools like Single-SPA, Bit, and now Webpack Module Federation have made microfrontends accessible. In 2025, we see growth in adoption, especially in medium-large companies.
Frameworks are adapting. There are emerging solutions for Vite (vite-plugin-federation) and discussions about native support in Next.js and other meta-frameworks.
The architecture will continue to evolve, but the core concept of team autonomy and independent deployments is here to stay.
If you are intrigued by architectures that enable scale, I recommend checking out another article: React Server Components: The New Era of Full-Stack Development where you will discover another revolutionary approach to structuring modern applications.
Let's go! π¦
π― Join Developers Who Are Evolving
Thousands of developers already use our material to accelerate their studies and achieve better positions in the market.
Why invest in structured knowledge?
Learning in an organized way with practical examples makes all the difference in your journey as a developer.
Start now:
- 2x of $13.08 on card
- or $24.90 at sight
"Excellent material for those who want to go deeper!" - John, Developer