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:
| Concern | Strategy |
|---|---|
| Authentication | Shared auth token (cookie or header), token refresh in shell |
| Routing | Shell owns top-level routes, micro-apps own sub-routes |
| Shared UI | Design system package published to npm, consumed independently |
| Communication | Custom events, shared event bus, or URL state |
| Error handling | Error boundaries per micro-app, centralized error reporting |
Trade-Off Matrix
| Factor | Monolith | Module Federation | iframes | Web Components |
|---|---|---|---|---|
| Team independence | Low | High | Highest | High |
| Performance | Best | Good | Poor (duplication) | Good |
| Shared state | Trivial | Moderate | Hard | Hard |
| Style isolation | None (by default) | None | Complete | Shadow DOM |
| Complexity | Low | High | Medium | Medium |
| SSR support | Full | Partial | None (per frame) | Limited |
Interview Signal
Micro-frontend questions test architectural maturity:
- Problem-first thinking â articulating the organizational problem before jumping to solutions
- Trade-off honesty â acknowledging that micro-frontends add real complexity and are not always worth it
- Integration depth â understanding the specific failure modes of each approach (shared dependency versioning, cross-app state, SSR)
- Experience markers â describing problems you've actually encountered, not just patterns you've read about