Back to blog

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:

  1. Deploy independence - Each part can be updated without affecting others
  2. Team autonomy - Different teams can work on different parts
  3. Technology agnostic - Each part can use different technologies
  4. 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

📖 View Complete Content

Comments (0)

This article has no comments yet 😢. Be the first! 🚀🦅

Add comments