DNA🏗ïļ System DesignMicro-Frontend Architecture
👑ApexArchitectureSystem DesignMicro-Frontends

Micro-Frontend Architecture

Micro-frontends let independent teams ship independently. But the complexity cost is real. Know when the trade-off is worth it.

Micro-Frontend Architecture

Micro-frontends extend the microservices idea to the frontend: independent teams own independent slices of the UI, each with its own deployment pipeline. It sounds great in theory. The reality is more nuanced.

When Micro-Frontends Make Sense

The architecture is justified when:

  • Multiple teams own different parts of the application (team boundaries map to UI boundaries)
  • Independent deployment is a hard requirement (team A ships without waiting for team B)
  • Technology heterogeneity is needed (legacy app migration, different framework requirements)
  • The app is large enough that monolith coordination costs exceed integration complexity

If you have one team or a small application, micro-frontends add overhead with no benefit.

Integration Approaches

Module Federation (Webpack 5 / Vite)

Runtime module sharing between independently built applications:

// Shell app (host) — webpack.config.js
new ModuleFederationPlugin({
  name: 'shell',
  remotes: {
    checkout: 'checkout@https://checkout.cdn.com/remoteEntry.js',
    catalog: 'catalog@https://catalog.cdn.com/remoteEntry.js',
  },
  shared: ['react', 'react-dom'],
});
 
// Checkout app (remote)
new ModuleFederationPlugin({
  name: 'checkout',
  filename: 'remoteEntry.js',
  exposes: { './CheckoutFlow': './src/CheckoutFlow' },
  shared: ['react', 'react-dom'],
});
// Shell app loads remote component
const CheckoutFlow = React.lazy(() => import('checkout/CheckoutFlow'));
 
function App() {
  return (
    <Suspense fallback={<Skeleton />}>
      <CheckoutFlow />
    </Suspense>
  );
}

Strengths: Shared dependencies (single React instance), lazy loading, type sharing possible. Weaknesses: Webpack/Vite coupling, version coordination for shared deps, debugging across boundaries is painful.

iframes

The simplest and most isolated approach:

function MicroFrontend({ url, title }: { url: string; title: string }) {
  return (
    <iframe
      src={url}
      title={title}
      style={{ width: '100%', height: '100%', border: 'none' }}
    />
  );
}

Communication via postMessage:

// Parent
iframe.contentWindow.postMessage({ type: 'UPDATE_CART', items }, targetOrigin);
 
// Child
window.addEventListener('message', (event) => {
  if (event.origin !== expectedOrigin) return;
  if (event.data.type === 'UPDATE_CART') updateCart(event.data.items);
});

Strengths: Complete isolation (CSS, JS, global state), any technology works, security via origin checks. Weaknesses: No shared dependencies (each iframe loads its own React), layout challenges (responsive sizing), accessibility gaps (focus management across frames), poor performance with many iframes.

Web Components

Custom elements as the integration layer:

class CheckoutElement extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' });
    const root = createRoot(shadow);
    root.render(<CheckoutFlow />);
  }
 
  static get observedAttributes() {
    return ['cart-id'];
  }
 
  attributeChangedCallback(name: string, _: string, newValue: string) {
    // Re-render with new props
  }
 
  disconnectedCallback() {
    // Cleanup
  }
}
 
customElements.define('checkout-flow', CheckoutElement);
<checkout-flow cart-id="abc123"></checkout-flow>

Strengths: Framework-agnostic, Shadow DOM provides style isolation, standard browser API. Weaknesses: React-in-custom-elements has rough edges (event handling, SSR), Shadow DOM complicates styling, no shared state without custom solution.

Route-Based Composition

The simplest micro-frontend: different routes serve different applications.

location /checkout {
  proxy_pass http://checkout-app;
}
location /catalog {
  proxy_pass http://catalog-app;
}
location / {
  proxy_pass http://shell-app;
}

Each app is a full SPA. Navigation between apps is a full page load (or soft navigation with a shared shell).

Strengths: Simplest to implement, complete independence, easy to reason about. Weaknesses: Full page loads between apps, shared UI (header, nav) must be duplicated or extracted.

Cross-Cutting Concerns

The hard part of micro-frontends isn't the integration — it's the shared concerns:

ConcernStrategy
AuthenticationShared auth token (cookie or header), token refresh in shell
RoutingShell owns top-level routes, micro-apps own sub-routes
Shared UIDesign system package published to npm, consumed independently
CommunicationCustom events, shared event bus, or URL state
Error handlingError boundaries per micro-app, centralized error reporting

Trade-Off Matrix

FactorMonolithModule FederationiframesWeb Components
Team independenceLowHighHighestHigh
PerformanceBestGoodPoor (duplication)Good
Shared stateTrivialModerateHardHard
Style isolationNone (by default)NoneCompleteShadow DOM
ComplexityLowHighMediumMedium
SSR supportFullPartialNone (per frame)Limited

Interview Signal

Micro-frontend questions test architectural maturity:

  1. Problem-first thinking — articulating the organizational problem before jumping to solutions
  2. Trade-off honesty — acknowledging that micro-frontends add real complexity and are not always worth it
  3. Integration depth — understanding the specific failure modes of each approach (shared dependency versioning, cross-app state, SSR)
  4. Experience markers — describing problems you've actually encountered, not just patterns you've read about