←Back to Blog
β€’7 min readβ€’Architecture

Micro-Frontends in 2025: Breaking Down Monolithic UIs

Learn how micro-frontend architecture enables teams to build scalable, independent frontend applications that work together seamlessly.

Micro-Frontends in 2025: Breaking Down Monolithic UIs

As applications grow, monolithic frontends become harder to maintain, test, and deploy. Micro-frontends offer a solution by decomposing UIs into smaller, independent applications that can be developed and deployed separately.

What Are Micro-Frontends?

Micro-frontends extend the microservices concept to frontend development. Instead of one large application, you have multiple smaller applications that:

  • Are developed independently
  • Can use different frameworks
  • Are deployed separately
  • Compose into a single user experience

Why Micro-Frontends?

1. Team Autonomy

Each team owns their domain end-to-end:

Team A: Product Catalog (React)
Team B: Shopping Cart (Vue)
Team C: Checkout (Svelte)
Team D: User Profile (Angular)

Teams can choose their tech stack and release independently.

2. Incremental Upgrades

Migrate from old to new technology gradually:

Legacy App (Angular 1.x) β†’ New App (React)
One micro-frontend at a time

3. Independent Deployment

Deploy changes without coordinating with other teams:

# Team A deploys product catalog
npm run deploy:catalog

# Doesn't affect Team B's cart or Team C's checkout

4. Better Scalability

Scale teams and code independently:

Large e-commerce app:
β”œβ”€β”€ catalog/ (30 developers)
β”œβ”€β”€ cart/ (10 developers)
β”œβ”€β”€ checkout/ (15 developers)
β”œβ”€β”€ profile/ (8 developers)
└── admin/ (12 developers)

Implementation Approaches

1. Build-Time Integration

Combine applications during build:

{
  "dependencies": {
    "@company/product-catalog": "^1.2.0",
    "@company/shopping-cart": "^2.1.0"
  }
}
import ProductCatalog from '@company/product-catalog';
import ShoppingCart from '@company/shopping-cart';

function App() {
  return (
    <>
      <ProductCatalog />
      <ShoppingCart />
    </>
  );
}

Pros: Simple, type-safe Cons: Requires rebuilding main app, tight coupling

2. Run-Time Integration via JavaScript

Load micro-frontends dynamically:

// Container app
const loadMicroFrontend = async (name, host) => {
  const script = document.createElement('script');
  script.src = `${host}/bundle.js`;
  script.onload = () => {
    window[name].mount('#app');
  };
  document.head.appendChild(script);
};

loadMicroFrontend('ProductCatalog', 'https://catalog.example.com');

Pros: True independence, dynamic loading Cons: Complex state sharing, no type safety

3. Run-Time Integration via Web Components

Use web standards:

// Product catalog micro-frontend
class ProductCatalog extends HTMLElement {
  connectedCallback() {
    this.innerHTML = '<h1>Products</h1>';
    // Render your app
  }
}

customElements.define('product-catalog', ProductCatalog);
<!-- Container app -->
<product-catalog></product-catalog>
<shopping-cart></shopping-cart>

Pros: Framework-agnostic, standard API Cons: Browser support, styling encapsulation challenges

4. Server-Side Composition (SSI/ESI)

Compose HTML on the server:

# Nginx config
location /shop {
  ssi on;

  # Compose fragments from different services
  <!--# include virtual="/catalog/fragment" -->
  <!--# include virtual="/cart/fragment" -->
}

Pros: SEO-friendly, progressive enhancement Cons: Server complexity, caching challenges

5. Module Federation (Webpack 5)

Share code and dependencies at runtime:

// Catalog app webpack.config.js
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'catalog',
      filename: 'remoteEntry.js',
      exposes: {
        './ProductList': './src/ProductList',
      },
      shared: ['react', 'react-dom'],
    }),
  ],
};
// Container app
const ProductList = lazy(() => import('catalog/ProductList'));

function App() {
  return (
    <Suspense fallback="Loading...">
      <ProductList />
    </Suspense>
  );
}

Pros: Powerful, optimized bundles, shared dependencies Cons: Webpack-specific, complex configuration

Communication Patterns

1. Custom Events

Simple, decoupled communication:

// Shopping cart emits event
window.dispatchEvent(new CustomEvent('cart:updated', {
  detail: { itemCount: 3 }
}));

// Header listens
window.addEventListener('cart:updated', (e) => {
  updateBadge(e.detail.itemCount);
});

2. Global State Store

Shared state across micro-frontends:

// Shared store
class GlobalStore {
  constructor() {
    this.state = { user: null, cart: [] };
    this.listeners = [];
  }

  subscribe(listener) {
    this.listeners.push(listener);
  }

  setState(updates) {
    this.state = { ...this.state, ...updates };
    this.listeners.forEach(l => l(this.state));
  }
}

window.__GLOBAL_STORE__ = new GlobalStore();

3. URL State

Share state via URL parameters:

// Catalog sets filter
history.pushState({}, '', '/shop?category=electronics');

// Cart reads filter
const params = new URLSearchParams(location.search);
const category = params.get('category');

4. Props/Attributes

Pass data through component props:

<product-catalog user-id="123" locale="en-US"></product-catalog>
class ProductCatalog extends HTMLElement {
  static observedAttributes = ['user-id', 'locale'];

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'user-id') {
      this.loadUserPreferences(newValue);
    }
  }
}

Shared Dependencies

1. Externalize Common Libraries

Avoid loading React multiple times:

// webpack.config.js
module.exports = {
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
  },
};
<!-- Load shared libraries once -->
<script src="https://cdn.example.com/react@18.js"></script>
<script src="https://cdn.example.com/react-dom@18.js"></script>

2. Module Federation Sharing

Automatically share dependencies:

shared: {
  react: { singleton: true, requiredVersion: '^18.0.0' },
  'react-dom': { singleton: true },
}

3. Import Maps

Browser-native module sharing:

<script type="importmap">
{
  "imports": {
    "react": "https://cdn.example.com/react@18.js",
    "react-dom": "https://cdn.example.com/react-dom@18.js"
  }
}
</script>

Styling Strategies

1. CSS Isolation

Use unique prefixes or CSS modules:

/* Product catalog */
.catalog__header { /* ... */ }
.catalog__product-card { /* ... */ }

/* Shopping cart */
.cart__header { /* ... */ }
.cart__item { /* ... */ }

2. Shadow DOM

True style encapsulation:

class ProductCard extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        .card { border: 1px solid #ccc; }
      </style>
      <div class="card">...</div>
    `;
  }
}

3. CSS-in-JS

Scoped styles automatically:

import styled from 'styled-components';

const Card = styled.div`
  border: 1px solid #ccc;
  /* Automatically scoped */
`;

4. Design System

Share common components and styles:

import { Button, Card, theme } from '@company/design-system';

function ProductCard() {
  return (
    <Card>
      <Button primary>Add to Cart</Button>
    </Card>
  );
}

Routing Strategies

1. Path-Based Routing

Each micro-frontend owns a path:

/products/** β†’ Product Catalog
/cart/** β†’ Shopping Cart
/checkout/** β†’ Checkout
/profile/** β†’ User Profile
// Container app router
switch (window.location.pathname) {
  case /^\/products/:
    loadMicroFrontend('catalog');
    break;
  case /^\/cart/:
    loadMicroFrontend('cart');
    break;
}

2. Fragment-Based Routing

Multiple micro-frontends per page:

/shop β†’ Container loads multiple fragments
  β”œβ”€β”€ Header (navigation micro-frontend)
  β”œβ”€β”€ Sidebar (filter micro-frontend)
  └── Main (product listing micro-frontend)

Real-World Examples

Spotify

Desktop app structure:
β”œβ”€β”€ Container (Electron shell)
β”œβ”€β”€ Browse (React)
β”œβ”€β”€ Search (React)
β”œβ”€β”€ Library (Preact)
β”œβ”€β”€ Player (Web Components)
└── Playlist (Vue)

IKEA

E-commerce platform:
β”œβ”€β”€ Product Search (Angular)
β”œβ”€β”€ Product Details (React)
β”œβ”€β”€ Shopping Cart (Vue)
β”œβ”€β”€ Checkout (Svelte)
└── My Account (Angular)

Challenges and Solutions

1. Performance Overhead

Problem: Loading multiple frameworks

Solutions:

  • Share common dependencies
  • Use lightweight frameworks (Preact, Svelte)
  • Lazy load micro-frontends
  • Server-side rendering

2. Consistency

Problem: Inconsistent UX across teams

Solutions:

  • Shared design system
  • Design tokens
  • Regular cross-team reviews
  • UX guidelines

3. Communication Complexity

Problem: Coordinating state across apps

Solutions:

  • Keep micro-frontends loosely coupled
  • Use event-driven architecture
  • Minimize shared state
  • Clear contracts/APIs

4. Testing

Problem: Integration testing across apps

Solutions:

  • Contract testing
  • End-to-end tests for critical flows
  • Visual regression testing
  • Staging environments

Best Practices

1. Start Small

Don't split everything immediately:

βœ“ Good: Split by business domain (catalog, cart, checkout)
βœ— Bad: Split every component into micro-frontend

2. Clear Boundaries

Define ownership clearly:

Team Catalog owns:
- Product search
- Product listings
- Product details
- Product reviews

Team Cart owns:
- Shopping cart
- Cart persistence
- Cart recommendations

3. Shared Nothing (Mostly)

Minimize sharing to reduce coupling:

Shared:
- Design system
- Authentication
- Common utilities

Not Shared:
- Business logic
- Domain models
- Internal state

4. Graceful Degradation

Handle micro-frontend failures:

try {
  await loadMicroFrontend('catalog');
} catch (error) {
  // Show fallback UI
  document.getElementById('app').innerHTML = `
    <div>Product catalog temporarily unavailable</div>
  `;
}

When to Use Micro-Frontends

Good fit:

  • Large applications with multiple teams
  • Different update cadences for features
  • Complex domains with clear boundaries
  • Legacy migration projects

Not a good fit:

  • Small teams or applications
  • Simple applications with few features
  • When team coordination is easy
  • Tight integration requirements

Conclusion

Micro-frontends offer a powerful pattern for scaling frontend development, but they come with complexity. The key is understanding your organization's needs and choosing the right level of independence.

Start with clear boundaries, simple communication patterns, and gradually increase sophistication as needed. Micro-frontends are a tool, not a goalβ€”use them when they solve real problems.

Is your application ready for micro-frontends?

πŸ‘¨β€πŸ’»

Jordan Patel

Web Developer & Technology Enthusiast