Back to Blog
Micro-frontendsArchitectureReactWeb Components

Micro-Frontends: Breaking Down Monolithic UIs

Learn how to architect and implement micro-frontends. From module federation to web components, discover patterns for scalable frontend development.

B
Bootspring Team
Engineering
February 26, 2026
3 min read

Micro-frontends extend microservices principles to frontend development. This architecture enables teams to build, deploy, and scale frontend applications independently.

What Are Micro-Frontends?#

Micro-frontends decompose a frontend application into smaller, independent units that can be developed and deployed by separate teams.

Traditional Monolith Micro-Frontend Architecture ┌─────────────────────┐ ┌──────┬──────┬──────┐ │ │ │ Team │ Team │ Team │ │ Single Frontend │ → │ A │ B │ C │ │ Application │ │ │ │ │ │ │ │ Auth │ Shop │ Blog │ └─────────────────────┘ └──────┴──────┴──────┘

Implementation Approaches#

1. Module Federation (Webpack 5)#

Module Federation allows loading remote modules at runtime:

1// host/webpack.config.js 2const ModuleFederationPlugin = require('@module-federation/enhanced/webpack'); 3 4module.exports = { 5 plugins: [ 6 new ModuleFederationPlugin({ 7 name: 'host', 8 remotes: { 9 shop: 'shop@http://localhost:3001/remoteEntry.js', 10 auth: 'auth@http://localhost:3002/remoteEntry.js', 11 }, 12 shared: ['react', 'react-dom'], 13 }), 14 ], 15}; 16 17// shop/webpack.config.js 18module.exports = { 19 plugins: [ 20 new ModuleFederationPlugin({ 21 name: 'shop', 22 filename: 'remoteEntry.js', 23 exposes: { 24 './ProductList': './src/components/ProductList', 25 './Cart': './src/components/Cart', 26 }, 27 shared: ['react', 'react-dom'], 28 }), 29 ], 30};

Use remote modules in the host:

1// host/src/App.jsx 2import React, { Suspense } from 'react'; 3 4const ProductList = React.lazy(() => import('shop/ProductList')); 5const LoginForm = React.lazy(() => import('auth/LoginForm')); 6 7function App() { 8 return ( 9 <div> 10 <Suspense fallback={<div>Loading...</div>}> 11 <LoginForm /> 12 <ProductList /> 13 </Suspense> 14 </div> 15 ); 16}

2. Web Components#

Framework-agnostic approach using custom elements:

1// product-card/index.js 2class ProductCard extends HTMLElement { 3 constructor() { 4 super(); 5 this.attachShadow({ mode: 'open' }); 6 } 7 8 static get observedAttributes() { 9 return ['product-id']; 10 } 11 12 async connectedCallback() { 13 const productId = this.getAttribute('product-id'); 14 const product = await this.fetchProduct(productId); 15 this.render(product); 16 } 17 18 render(product) { 19 this.shadowRoot.innerHTML = ` 20 <style> 21 .card { 22 border: 1px solid #ddd; 23 padding: 16px; 24 border-radius: 8px; 25 } 26 .price { 27 color: #2563eb; 28 font-weight: bold; 29 } 30 </style> 31 <div class="card"> 32 <h3>${product.name}</h3> 33 <p class="price">$${product.price}</p> 34 <button>Add to Cart</button> 35 </div> 36 `; 37 38 this.shadowRoot.querySelector('button').addEventListener('click', () => { 39 this.dispatchEvent(new CustomEvent('add-to-cart', { 40 detail: { productId: product.id }, 41 bubbles: true, 42 })); 43 }); 44 } 45} 46 47customElements.define('product-card', ProductCard);

3. Single-SPA Framework#

Orchestrate multiple frameworks:

1// root-config.js 2import { registerApplication, start } from 'single-spa'; 3 4registerApplication({ 5 name: '@company/navbar', 6 app: () => System.import('@company/navbar'), 7 activeWhen: ['/'], 8}); 9 10registerApplication({ 11 name: '@company/shop', 12 app: () => System.import('@company/shop'), 13 activeWhen: ['/shop'], 14}); 15 16registerApplication({ 17 name: '@company/blog', 18 app: () => System.import('@company/blog'), 19 activeWhen: ['/blog'], 20}); 21 22start();

Shared State Management#

Custom Event Bus#

1// shared/eventBus.js 2class EventBus { 3 constructor() { 4 this.events = {}; 5 } 6 7 subscribe(event, callback) { 8 if (!this.events[event]) { 9 this.events[event] = []; 10 } 11 this.events[event].push(callback); 12 13 return () => { 14 this.events[event] = this.events[event].filter(cb => cb !== callback); 15 }; 16 } 17 18 publish(event, data) { 19 if (!this.events[event]) return; 20 this.events[event].forEach(callback => callback(data)); 21 } 22} 23 24window.__EVENT_BUS__ = window.__EVENT_BUS__ || new EventBus(); 25export default window.__EVENT_BUS__;

When to Use Micro-Frontends#

Good fit:

  • Large teams (10+ developers)
  • Multiple business domains
  • Need for independent deployments
  • Legacy migration scenarios

Avoid when:

  • Small teams or projects
  • Tight performance requirements
  • Simple applications

Conclusion#

Micro-frontends provide organizational scalability at the cost of technical complexity. Start with a clear organizational need and choose the simplest integration approach that meets your requirements.

Share this article

Help spread the word about Bootspring