ESC
Type to search guides, tutorials, and reference documentation.
Verified by Garnet Grid

Micro-Frontend Architecture: Scaling Frontend Development Across Teams

Implement micro-frontend patterns that let independent teams build, deploy, and own frontend features without coordinating monolithic releases. Covers Module Federation, single-spa, Web Components, shared design systems, and the organizational patterns that make micro-frontends succeed.

Micro-frontends apply the microservices principle to the frontend: each team owns a vertical slice of the application — from database to UI — and deploys it independently. Team A ships their checkout flow on Tuesday. Team B ships their product catalog on Thursday. Neither team waits for the other. Neither team coordinates a shared release train.

This is not a technology problem. It is an organizational scaling problem. Micro-frontends become valuable when a single frontend codebase has become the bottleneck for multiple teams.


When Micro-Frontends Make Sense

Good Fit

  • 3+ teams contributing to the same frontend
  • Teams blocked on each other’s release schedules
  • Different features have different performance/technology requirements
  • Teams need deployment independence

Bad Fit

  • Single team building the frontend
  • Application is small enough to fit in one codebase
  • Consistency across the application is the top priority
  • Team does not have the infrastructure maturity to operate independently

Starting with a monolithic frontend and extracting micro-frontends when scaling pain appears is almost always better than starting with micro-frontends.


Integration Patterns

Build-Time Integration (Module Federation)

Webpack Module Federation allows applications to share modules at runtime without bundling them together at build time:

// Host application (shell)
// webpack.config.js
new ModuleFederationPlugin({
  name: 'shell',
  remotes: {
    checkout: 'checkout@https://checkout.example.com/remoteEntry.js',
    catalog: 'catalog@https://catalog.example.com/remoteEntry.js',
  },
  shared: {
    react: { singleton: true, requiredVersion: '^18.0.0' },
    'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
  },
});
// Host application - consuming a remote component
const Checkout = React.lazy(() => import('checkout/CheckoutForm'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <Checkout orderId={orderId} />
    </Suspense>
  );
}

Pros: Familiar developer experience, shared bundles reduce duplication, works with existing React/Vue/Angular patterns. Cons: Tight coupling to Webpack/Vite, shared dependency version management.

Runtime Integration (single-spa)

single-spa is a meta-framework that orchestrates multiple frontend applications in the browser:

// root-config.js
registerApplication({
  name: '@org/checkout',
  app: () => System.import('@org/checkout'),
  activeWhen: '/checkout',
});

registerApplication({
  name: '@org/catalog',
  app: () => System.import('@org/catalog'),
  activeWhen: '/products',
});

Each micro-frontend is a fully independent application with its own build pipeline. The root config loads and unloads them based on route.

Pros: Complete technology independence (React + Vue + Angular in one app), independent deployments. Cons: Larger aggregate bundle size, complex routing, CSS isolation challenges.

Server-Side Composition

Services render HTML fragments that are composed on the server:

<!-- Server-assembled page -->
<html>
  <body>
    <header>
      <!-- Fragment from header-service -->
      ${await fetch('http://header-service/fragment')}
    </header>
    <main>
      <!-- Fragment from product-service -->
      ${await fetch('http://product-service/fragment?id=123')}
    </main>
    <aside>
      <!-- Fragment from recommendation-service -->
      ${await fetch('http://recommendation-service/fragment?user=456')}
    </aside>
  </body>
</html>

Pros: Works with any technology, great for SEO, fast initial page load. Cons: No client-side interactivity between fragments without additional JavaScript.

Web Components

Use native Web Components as the integration boundary:

// Checkout team's Web Component
class CheckoutWidget extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>/* Scoped styles */</style>
      <form>...</form>
    `;
  }
}
customElements.define('checkout-widget', CheckoutWidget);
<!-- Host application -->
<script src="https://checkout.example.com/widget.js"></script>
<checkout-widget order-id="123"></checkout-widget>

Pros: Native browser standard, built-in CSS isolation via Shadow DOM, framework agnostic. Cons: Limited React/Vue/Angular interoperability, Shadow DOM styling limitations.


Shared Concerns

Design System

A shared design system is non-negotiable. Without it, each micro-frontend looks like it was built by a different company:

@org/design-system (npm package)
├── components/
│   ├── Button/
│   ├── Input/
│   ├── Modal/
│   └── ...
├── tokens/
│   ├── colors.css
│   ├── spacing.css
│   └── typography.css
└── themes/
    ├── light.css
    └── dark.css

The design system is published as a versioned npm package. Each team pins to a version and upgrades on their own schedule.

Cross-Cutting Navigation

Navigation, authentication state, and user context must be globally available:

// Shared event bus for cross-micro-frontend communication
const eventBus = {
  emit(event, data) {
    window.dispatchEvent(new CustomEvent(event, { detail: data }));
  },
  on(event, callback) {
    window.addEventListener(event, (e) => callback(e.detail));
  },
};

// Checkout emits when order is placed
eventBus.emit('order:placed', { orderId: '123', total: 299.99 });

// Analytics micro-frontend listens
eventBus.on('order:placed', (data) => {
  trackConversion(data.orderId, data.total);
});

Performance Budget

Each micro-frontend gets a performance budget:

Total page budget:     500 KB (compressed)
Shell:                  50 KB
Shared design system:   80 KB
Per micro-frontend:    120 KB max

Monitor bundle sizes in CI and fail builds that exceed the budget.


Deployment Independence

The core benefit of micro-frontends — and the hardest to achieve — is independent deployment:

┌────────────────────────────────────────┐
│            CDN / Edge                  │
├────────────┬────────────┬──────────────┤
│  Shell     │  Checkout  │  Catalog     │
│  v2.1.0    │  v3.4.2    │  v1.8.0      │
│  (Mon)     │  (Tue)     │  (Thu)       │
└────────────┴────────────┴──────────────┘

Each micro-frontend is deployed to its own URL/CDN path. The shell application loads the latest version at runtime. No coordination needed.


Anti-Patterns

Anti-PatternConsequenceFix
Shared mutable stateCoupling between micro-frontendsEvents and props only, no shared global state
Inconsistent UXApp feels like 5 different productsShared design system, design reviews
Nano-frontendsEvery button is its own micro-frontendAlign boundaries to team/domain ownership
Shared databaseBackend coupling negates frontend independenceEach team owns their API and data
No versioningBreaking changes cascadeSemantic versioning for shared contracts

Micro-frontends are an organizational pattern, not a technical one. They succeed when team boundaries align with domain boundaries. They fail when used to solve problems that are better addressed by better communication, shared conventions, or a well-maintained monolith.

Jakub Dimitri Rezayev
Jakub Dimitri Rezayev
Founder & Chief Architect • Garnet Grid Consulting

Jakub holds an M.S. in Customer Intelligence & Analytics and a B.S. in Finance & Computer Science from Pace University. With deep expertise spanning D365 F&O, Azure, Power BI, and AI/ML systems, he architects enterprise solutions that bridge legacy systems and modern technology — and has led multi-million dollar ERP implementations for Fortune 500 supply chains.

View Full Profile →