Blog19 min read

Micro Frontends and Backend for Frontend: Architecture, Module Federation, and Interview Guide (2026)

Micro frontends and backend for frontend architecture guide for 2026

Micro frontends split one web app into pieces that separate teams build, test, and deploy on their own. The backend for frontend (BFF) pattern is its server-side partner: a thin API layer owned by the frontend team and shaped for one client. This guide covers both the way you’d need them at work and in a frontend system design interview.

Both patterns solve an organizational problem first and a technical one second. That’s the single most useful idea to bring into an interview: you choose them when teams block each other, not because the code is big.

What to Expect

  • What micro frontends are, and how they compare with a monolith and a modular monolith.
  • Micro frontend architecture: how to split an app, the app shell, and routing.
  • Every composition approach (build time, server, edge, iframes, module federation, web components) in one comparison table, with config you can copy.
  • Shared dependencies, cross-app communication, styling, and design systems.
  • The backend for frontend pattern, how it differs from an API gateway, and how it pairs with micro frontends.
  • Trade-offs, when not to use either pattern, and interview questions with strong sample answers.

Jump to a section


What Are Micro Frontends?

Diagram: micro frontends vs monolith, one frontend codebase compared with independent team-owned apps composed in an app shell

Micro frontends apply the microservices idea to the browser. Instead of one frontend codebase that every team commits to, each team owns a slice of the product end to end: its UI, its release pipeline, and often its own backend. A host application (the app shell) puts the slices together at build time, on the server, or in the browser.

The term was popularized around 2016, and Cam Jackson’s article on martinfowler.com is still the standard reference for the core definitions.

The three properties that make it a micro frontend

  • Independent deployment. A team ships its slice without rebuilding or redeploying the others. If every release still needs a coordinated deploy, you have a distributed monolith, not micro frontends.
  • Team ownership. One team owns a business capability (search, checkout, account), not a technical layer (all buttons, all API calls).
  • Isolation by default. Slices don’t share runtime state, global CSS, or internal modules. They talk through a small, explicit contract.

Micro frontends vs monolith vs modular monolith

Most “micro frontends vs monolith” debates skip the middle option. A modular monolith (one repo, one deploy, strict module boundaries enforced by lint rules or a monorepo tool) gives you most of the code-organization benefits with none of the runtime cost.

Aspect Monolith Modular monolith Micro frontends
Deploys One, shared One, shared One per team
Boundaries By convention Enforced at build time Enforced at runtime
Framework choice One One Can differ (usually shouldn’t)
Runtime overhead None None Extra requests, duplicate code risk
Best fit 1-3 teams 3-8 teams in one release train Many teams that need separate release cycles

The team counts are rules of thumb, not thresholds. The real signal is release friction: a merge queue measured in hours, or one team’s bug blocking everyone else’s launch.

What problems do micro frontends actually solve?

They solve coordination cost. With dozens of engineers in one frontend, builds get slow, ownership of shared files gets blurry, and a release is only as fast as its slowest team. Splitting by business domain lets each team pick its own pace and roll back without touching anyone else.

They also make incremental migration possible. You can wrap a legacy AngularJS or jQuery app in a shell and replace it route by route with React, instead of betting on a full rewrite.

What they don’t solve: slow pages, messy components, or a weak design system. Those get worse if you split before fixing them.


Micro Frontend Architecture: How to Split an App

Diagram: micro frontend architecture with an app shell routing to team-owned search, product, checkout, and account apps

Find the system design framework here: System Design Interview Framework (RADIO) ->

A micro frontend architecture has two parts: the app shell, and the slices it hosts. Most of the hard decisions are about where you draw the lines between slices.

Vertical split vs horizontal split

A vertical split gives each team whole pages or routes: /search, /product/:id, /checkout. Only one micro frontend is on screen at a time (plus the shell), so there’s little runtime interaction to manage. Start here.

A horizontal split puts several micro frontends on the same page: a product page with a recommendations widget from one team and a reviews widget from another. It’s more flexible and much harder: shared layout, competing data fetches, and cross-widget events.

Aspect Vertical (per route) Horizontal (per fragment)
Complexity Low High
Communication needs URL and a few events Frequent events or shared state
Performance risk Full navigation between apps Several bundles and fetches per page
Typical use E-commerce, admin consoles Dashboards, portals, homepages

Draw boundaries around business capabilities

Good boundaries follow domain-driven design: a slice maps to a capability a single team can own, with its own data and vocabulary. “Checkout” is a good boundary. “Header” usually isn’t, because every team needs to change it.

A quick test: if a typical feature needs changes in three micro frontends, the boundaries are wrong.

What does the app shell own?

Keep the shell thin. It should own only what genuinely spans every slice:

  • Top-level routing: which micro frontend mounts for which URL.
  • Authentication and the session, exposed to slices through a small API.
  • The global layout frame (header, navigation), ideally from the shared design system.
  • Loading, error boundaries, and fallbacks when a remote fails.
  • Cross-cutting telemetry: page views, errors, and Core Web Vitals per slice.

Business logic in the shell turns it back into a monolith that every team has to deploy through.

Routing between micro frontends

The shell owns the first URL segment, and each micro frontend owns everything below it. The checkout app never needs to know the search app’s routes, only the public URLs it links to.

const routes = [
  { prefix: '/search', app: 'search' },
  { prefix: '/checkout', app: 'checkout' },
  { prefix: '/account', app: 'account' },
];

function resolveApp(pathname) {
  const match = routes.find((r) => pathname === r.prefix || pathname.startsWith(r.prefix + '/'));
  return match ? match.app : 'not-found';
}

console.log(resolveApp('/checkout/payment')); // checkout
console.log(resolveApp('/searching'));        // not-found

Matching on the segment boundary (prefix + '/') matters: a naive startsWith('/search') would also send /searching to the search team. Libraries like single-spa implement this “activity function” idea for you.


Composition Approaches and Module Federation

Diagram: micro frontend composition approaches, build time, server side, edge, iframes, module federation, and web components

Find related rendering trade-offs here: SSR vs CSR ->

Composition is how the slices end up on one page. Each option moves the integration point to a different place, and that choice decides your performance, isolation, and how independent deploys really are.

The six composition approaches compared

Approach Where it composes Independent deploy? Main cost
npm packages Build time No: the host must rebuild Lockstep releases
Server-side includes / templates Origin server Yes Interactivity needs extra client code
Edge-side includes CDN Yes CDN-specific, harder to debug
iframes Browser Yes Poor UX, SEO, and accessibility
Module federation Browser (or server) at runtime Yes Shared-dependency versioning
Web components Browser at runtime Yes SSR and React interop friction

Build-time composition (npm packages)

Each team publishes its slice as a package, and the host installs it. It’s simple and fully type-checked, but it isn’t really micro frontends: a change in checkout ships only when the host bumps the version and redeploys. Use it for shared libraries and the design system, not for product slices.

Server-side and edge composition

The server (or CDN) fetches HTML fragments from each team and stitches them into one response. The browser gets a fully rendered page, so this is the best option for SEO and first paint. Each fragment then hydrates its own interactive parts.

Edge-side includes (ESI) and edge functions do the same stitching closer to the user and let each fragment have its own cache lifetime: a product description can be cached for hours while the cart fragment stays uncached.

iframes

iframes give the strongest isolation the browser offers: separate JavaScript realm, separate CSS, and an origin boundary. They’re still the right call for untrusted third-party content and some embedded admin tools. For product UI they hurt: each frame needs a fixed or measured height, deep linking and history are awkward, focus management breaks, and crawlers treat the content as a separate page.

Module federation

Module federation lets one JavaScript build load modules from another build at runtime, while sharing common libraries like React. It started as a webpack 5 feature and is now also available for Rspack, Vite, and others through the Module Federation 2.0 project. Each app is a remote (it exposes modules), a host (it consumes them), or both.

The checkout team exposes its page from its own build:

// checkout/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'checkout',
      filename: 'remoteEntry.js',
      exposes: { './CheckoutPage': './src/CheckoutPage' },
      shared: {
        react: { singleton: true, requiredVersion: '^18.3.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.3.0' },
      },
    }),
  ],
};

The shell declares the remote and loads it lazily, like any code-split chunk:

// shell/webpack.config.js (plugin options)
// remotes: { checkout: 'checkout@https://checkout.example.com/remoteEntry.js' }

// shell/src/App.jsx
const CheckoutPage = React.lazy(() => import('checkout/CheckoutPage'));

function CheckoutRoute() {
  return (
    <ErrorBoundary fallback={<CheckoutUnavailable />}>
      <Suspense fallback={<PageSkeleton />}>
        <CheckoutPage />
      </Suspense>
    </ErrorBoundary>
  );
}

Three details interviewers probe:

  • singleton: true forces one copy of React. Two copies break hooks, because each copy has its own dispatcher.
  • remoteEntry.js must not be cached long-term. It’s the manifest that points to the hashed chunks, so cache it briefly or revalidate it, and cache the hashed chunks forever.
  • The error boundary is mandatory. A remote is a network dependency owned by another team; when it fails, the shell should show a fallback, not a blank page.

Hard-coding remote URLs in the shell’s build couples deploys again. Production setups usually read them from a runtime manifest (a JSON file or service), so a team can release or roll back by updating one entry. Read more in the webpack module federation docs.

Web components

Each team ships a custom element (<checkout-app>), and the shell just renders the tag. Custom elements are a browser standard, so the contract is framework-neutral, and Shadow DOM can scope styles. The friction: server rendering needs Declarative Shadow DOM, and passing rich data and events between React and custom elements needs care (React 19 improved property and event support for custom elements).

Import maps and single-spa

Import maps let the browser resolve bare specifiers like import 'checkout' to a URL you control at runtime, which gives you independent deploys with native ES modules and no bundler plugin. single-spa is a framework-agnostic router that mounts and unmounts micro frontends by URL and is often paired with import maps.

Which composition approach should you pick?

  • SEO-critical, content-heavy pages: server-side or edge composition.
  • One framework across teams with a client-rendered app: module federation.
  • Mixed frameworks during a migration: web components or single-spa.
  • Untrusted or third-party UI: iframes.
  • Shared UI kits and utilities: npm packages.

Mixing is normal: server composition for the page frame and module federation for interactive widgets inside it.


Shared Dependencies, Communication, and Styling

Diagram: micro frontends communicating through URL, events, and a shared design system instead of shared global state

Find state architecture patterns here: MVVM Design Pattern in React ->

Most micro frontend failures happen in the seams: two versions of React on one page, a global CSS rule leaking into another team’s UI, or a shared Redux store that couples every release. Treat the seams as a public API.

How should micro frontends communicate?

Use the weakest coupling that works, in this order:

  1. The URL. Route params and query strings are shareable, bookmarkable, and need no shared code.
  2. Props from the shell. The shell passes a small, versioned set of inputs (user id, locale, feature flags) when it mounts a slice.
  3. Browser events. A slice publishes a CustomEvent like cart:item-added; any slice can listen. Neither side imports the other.
  4. Shared state. A last resort, and only for truly global data like the session.

A tiny typed event bus is enough for most products. In the browser you’d use window.dispatchEvent(new CustomEvent(...)) (see MDN CustomEvent); this version runs anywhere:

function createEventBus() {
  const handlers = new Map();
  return {
    on(type, fn) {
      if (!handlers.has(type)) handlers.set(type, new Set());
      handlers.get(type).add(fn);
      return () => handlers.get(type).delete(fn); // unsubscribe on unmount
    },
    emit(type, detail) {
      (handlers.get(type) || []).forEach((fn) => fn(detail));
    },
  };
}

const bus = createEventBus();
const off = bus.on('cart:item-added', (d) => console.log('header badge +', d.qty));
bus.emit('cart:item-added', { sku: 'A1', qty: 2 }); // header badge + 2
off();
bus.emit('cart:item-added', { sku: 'B2', qty: 1 }); // (nothing logged)

Namespace event names by owning team (cart:, search:) and document their payloads like an API. Always unsubscribe on unmount, or a slice that’s been navigated away from keeps reacting.

Sharing dependencies without coupling deploys

Dependency Strategy Why
React, React DOM Shared singleton Two copies break hooks and context
Design system Shared, semver range Consistent UI, one download
Router, data clients Owned per slice Internal choices stay internal
Utilities (dates, lodash) Bundle per slice Small, avoids version negotiation

Share as little as you can. Every shared dependency is a contract that every team must upgrade together, which is the coupling you were trying to remove.

Styling and the design system

Global CSS is the most common leak. Pick one isolation technique and enforce it across teams:

  • CSS Modules or a build-time CSS-in-JS tool that hashes class names.
  • A required class prefix per team (.chk-, .srch-), checked by a lint rule.
  • Shadow DOM, when slices are web components.

Visual consistency comes from a shared design system and design tokens (CSS custom properties for color, spacing, and type), not from shared global styles.

Authentication across micro frontends

The shell owns login and the session. Prefer an HttpOnly, Secure session cookie on a shared parent domain, so every slice’s API calls are authenticated without any slice touching a token. If tokens are unavoidable, the shell exposes a getAccessToken() function rather than putting a token in localStorage, where any script on the page (including another team’s dependency) can read it. More in Frontend Security.


The Backend for Frontend (BFF) Pattern

Diagram: backend for frontend pattern with separate web and mobile BFFs aggregating calls to shared downstream services

Find API design guidance here: API Design ->

A backend for frontend is a server-side layer built for one specific client experience: one BFF for the web app, another for the iOS and Android apps. It sits between the client and the general-purpose services, and it’s owned by the team that owns the client. Sam Newman popularized the name, based on how SoundCloud split its APIs per client.

What problem does the BFF pattern solve?

A single general-purpose API serves every client the same way, so every client pays for it:

  • Chatty clients. A product page needs product, price, inventory, and reviews: four round trips from a phone on a slow network.
  • Over-fetching. The mobile app downloads fields only the desktop page renders.
  • Cross-team blocking. The frontend team waits on a backend team to add a field or an endpoint.

A BFF fixes all three: it fans out to downstream services in parallel on a fast internal network, trims and reshapes the response for its client, and lets the frontend team ship API changes alongside UI changes.

A minimal BFF endpoint

// Simulated downstream services
const services = {
  product: async (id) => ({ id, name: 'Desk Lamp', description: 'Long text...', internalSku: 'X-77' }),
  price: async (id) => ({ amount: 4900, currency: 'USD' }),
  reviews: async (id) => { throw new Error('reviews timeout'); },
};

async function getProductPage(id) {
  const [product, price, reviews] = await Promise.allSettled([
    services.product(id),
    services.price(id),
    services.reviews(id),
  ]);
  if (product.status === 'rejected') throw product.reason; // product is required
  return {
    title: product.value.name,
    price: price.status === 'fulfilled' ? price.value : null,
    reviews: reviews.status === 'fulfilled' ? reviews.value : [], // optional: degrade
  };
}

getProductPage('42').then((page) => console.log(page));
// { title: 'Desk Lamp', price: { amount: 4900, currency: 'USD' }, reviews: [] }

The details are the point: calls run in parallel, internal fields like internalSku never reach the browser, and an optional dependency failing degrades the page instead of failing it. Promise.allSettled makes that required-vs-optional decision explicit.

What belongs in a BFF, and what doesn’t

Belongs in the BFF Belongs in downstream services
Aggregating and reshaping responses for one UI Business rules (pricing, eligibility, permissions)
Session handling and token exchange Source-of-truth data and writes
Client-specific caching and pagination shapes Logic shared by every client
Feature-flag evaluation for the UI Long-running jobs and workflows

If two BFFs start duplicating the same business rule, that rule belongs in a downstream service.

BFF vs API gateway vs GraphQL

Aspect API gateway BFF GraphQL layer
Serves All clients One client experience All clients, per-query shape
Owned by Platform team The frontend team Usually a platform or API team
Main job Routing, auth, rate limits Aggregation and shaping Client-selected fields
Risk Becomes a bottleneck Duplication across BFFs Query cost and caching complexity

These aren’t exclusive. A common setup is a gateway for cross-cutting concerns in front of per-client BFFs, and some teams implement their BFF as a GraphQL server.

BFFs and security

A BFF is also the cleanest way to keep tokens out of the browser. The browser holds only an HttpOnly session cookie; the BFF stores the OAuth access and refresh tokens server-side and attaches them to downstream calls. Because the browser now authenticates with a cookie, protect state-changing routes against CSRF with SameSite cookies plus a CSRF token or origin check. See XSS vs CORS for the browser side.

How BFFs pair with micro frontends

The two patterns line up naturally: each micro frontend team owns its slice and its BFF, so a feature that needs a new field ships end to end without another team. Next.js route handlers, Remix loaders, and React Server Components play a similar role inside one framework: server code, owned by the frontend team, that shapes data for the UI.

One BFF per micro frontend can mean a lot of services. Many teams start with one BFF per client type and split only when a team’s API changes keep blocking another’s.


Trade-offs and When Not to Use Micro Frontends

Diagram: micro frontends trade-offs, team autonomy and fast releases weighed against complexity and bundle size

Find performance techniques here: Code Splitting and Lazy Loading ->

Micro frontends and BFFs trade technical simplicity for team autonomy. That’s a good trade only when autonomy is your bottleneck.

What you gain and what you pay

You gain You pay
Independent releases and rollbacks More pipelines, environments, and monitoring
Clear ownership per domain Cross-team contracts to version and document
Incremental migration off legacy code Risk of duplicate libraries and larger downloads
Smaller, faster builds per team Harder end-to-end testing and local setup
Failure isolation between slices UX drift if the design system is weak

When not to use micro frontends

  • A small team. If everyone fits in one standup, the coordination problem doesn’t exist yet.
  • A tightly connected UI. Editors, design tools, and spreadsheets share state on every keystroke. Splitting them adds latency to every interaction.
  • Unclear domains. Splitting before the product’s boundaries settle means redrawing them later, across repos.
  • A performance-critical landing page where every kilobyte of JavaScript costs conversions, unless you compose on the server.
  • “Each team picks its framework” as the main goal. Running React, Vue, and Angular on one page downloads three runtimes. Allow it for migrations, not as policy.

When not to use a BFF

  • You have one client, and the existing API already fits it.
  • Nobody on the frontend team can be on call for a server.
  • The BFF would just proxy calls one to one, adding a network hop without aggregating anything.

Keeping performance in check

  • Set a JavaScript budget per slice and fail the build when it’s exceeded.
  • Share the framework as a singleton, and audit for duplicate packages across remotes.
  • Preload the next likely remote (<link rel="modulepreload"> or prefetch on hover) so route changes don’t wait on a cold remoteEntry.js.
  • Compose on the server for above-the-fold content, so first paint doesn’t wait on several client bundles.
  • Track Core Web Vitals per slice, so a regression points to its owning team.

Testing across independent deploys

End-to-end tests across every team’s latest build are slow and flaky. Lean on contract tests instead: each slice tests the events it emits and the props it accepts against a shared schema, and the shell runs a small smoke suite against production remotes after every deploy. Pair it with feature flags (see Design a Feature Flag SDK) so a risky slice can be turned off without a redeploy.


Micro Frontends and BFF Interview Questions

Diagram: a micro frontends system design interview flow, from requirements to boundaries, composition, data layer, and trade-offs

Find in-depth system design practice here: Frontend System Design Interview Handbook ->

These questions come up in senior frontend and architecture rounds, either directly or as a follow-up in a system design question like “design an e-commerce site for 20 teams”. Interviewers want a decision rule, not a definition.

1. What are micro frontends, and why would a company adopt them?

Micro frontends split a frontend into independently deployable apps, each owned by one team around a business capability. Companies adopt them to cut coordination cost when many teams share one frontend: separate releases, rollbacks, and builds. A strong answer adds that it’s an organizational fix, and names the costs (runtime overhead, consistency, ops) in the same breath.

2. How would you split an e-commerce frontend into micro frontends?

Split vertically by business capability and route: discovery (home, search, listings), product detail, cart and checkout, and account. A thin shell owns routing, the session, the header, and error fallbacks. Cross-slice needs, like the cart badge in the header, go through a documented event such as cart:updated.

“I’d start vertical, one team per route, because it keeps runtime interaction near zero. I’d only add horizontal fragments, like a recommendations widget on the product page, when a separate team owns that capability and needs to ship it independently.”

Practice the full design: Design E-Commerce Platform.

3. Module federation vs web components vs iframes: when do you use each?

Use module federation when teams share one framework and you want runtime loading with a shared React copy. Use web components when frameworks differ or you need a framework-neutral contract, such as during a migration. Use iframes only for untrusted or third-party content, where hard isolation outweighs the UX cost. Mention server or edge composition as the answer when SEO and first paint matter most.

4. How do micro frontends share state?

Mostly, they shouldn’t. Pass shared context (user, locale, flags) from the shell as props, use the URL for navigation state, and use namespaced browser events for notifications between slices. A global store shared by all slices recreates the monolith’s coupling with worse tooling.

What the interviewer is checking: whether you treat the seams as an API with versioning, rather than reaching for shared Redux.

5. How do you prevent two copies of React from loading?

Declare React and React DOM as shared with singleton: true and a compatible requiredVersion range in every module federation config. At runtime, the first loaded compatible version wins and the others reuse it. Add a CI check that flags remotes whose React range no longer overlaps with the shell’s, because a mismatch either loads a second copy or warns at runtime depending on configuration.

6. How do you keep a consistent look across teams?

A shared design system with design tokens, distributed as a versioned package (or a shared federated module), plus scoped styles so no team writes global CSS. Visual regression tests on the design system catch drift before it spreads to every slice.

7. What happens when one micro frontend fails to load?

The shell catches it with an error boundary around each remote and shows a fallback, like “Reviews are unavailable” or a link to retry. The rest of the page keeps working. Log the failure with the remote’s name and version so the owning team gets the alert, and keep the last known good remote URL in the runtime manifest for fast rollback.

8. How do you handle versioning and rollbacks?

Each slice builds immutable, content-hashed assets. A runtime manifest maps each remote name to its current remoteEntry.js URL. Releasing is updating that entry, and rolling back is pointing it at the previous build: seconds, with no shell redeploy. Breaking changes to events or props get a new version name and a deprecation window.

9. What is the backend for frontend pattern?

A backend for frontend is an API layer dedicated to one client experience and owned by that client’s team. It aggregates downstream services, reshapes data for the UI, and handles session and token logic. It reduces round trips and over-fetching and lets frontend teams change their API without waiting on backend teams.

10. BFF vs API gateway: what’s the difference?

An API gateway is one shared entry point for all clients, focused on cross-cutting concerns: routing, authentication, rate limiting. A BFF is per client and focused on aggregation and response shape. Many systems use both: the gateway in front, BFFs behind it.

11. When would you not introduce a BFF?

When there’s one client and the existing API fits it, when the BFF would only proxy one-to-one, or when the frontend team can’t operate a service in production. In those cases a BFF adds a hop, a deploy, and an on-call rotation for no gain.

12. How does a BFF improve security for single-page apps?

It keeps OAuth tokens on the server. The browser holds only an HttpOnly, Secure, SameSite session cookie, so an XSS bug can’t read and exfiltrate a long-lived token. The trade-off is that cookie-based auth needs CSRF protection on state-changing requests.

13. How would you migrate a legacy monolith to micro frontends?

Use the strangler fig pattern. Put a shell (or a reverse proxy) in front of the legacy app, then move one route at a time to a new micro frontend while the legacy app serves everything else. Pick a low-risk, high-change route first to prove the pipeline, and retire the legacy app only when its last route moves.

14. What’s the junior vs senior answer to “should we use micro frontends”?

Junior answer Senior answer
“Yes, it scales better.” “It depends on team count and release friction. How many teams ship to this app, and what blocks them today?”
Lists benefits only Names the costs: bundle size, consistency, testing, ops
Picks module federation by default Picks composition by constraints: SEO, frameworks, trust
Ignores the backend Pairs each slice with data ownership, often a BFF

15. How would you measure whether the migration worked?

Measure the problem you set out to fix: deploy frequency per team, lead time from merge to production, rollback time, and how often one team’s change blocks another’s. Guard the costs with per-slice JavaScript size and Core Web Vitals. If delivery speed didn’t improve and the page got slower, the split didn’t pay off.


How to Use This Guide in Your Prep

If you’re short on time, learn the vertical split, the composition table, and the BFF vs API gateway difference first. Those three cover most interview follow-ups. Then practice saying when not to use either pattern, because that’s the answer that sounds senior.

Next, practice applying the patterns inside a full design:

Related reading: Frontend frameworks compared: React vs Vue vs Angular vs Svelte · Frontend vs backend: differences, skills, and pay