How Microfrontends Work: Architecture, Patterns and Examples

Última actualización: 12/01/2025
  • Microfrontends apply microservice principles to the browser UI, splitting large frontends into autonomous, domain‑oriented slices owned by cross‑functional teams.
  • Integration relies on the DOM, Custom Elements, Module Federation and an application shell that orchestrates routing, security, composition and shared libraries.
  • Frameworks like React, Angular and Next.js, together with patterns such as BFF, event buses and lazy loading, enable scalable, resilient and observable microfrontend systems.
  • Microfrontends add architectural complexity but pay off in large, multi‑team products where independent deployment, gradual migration and differentiated scaling are crucial.

microfrontends architecture illustration

Microfrontends have become one of the most talked‑about frontend architecture patterns for teams that outgrew the classic single-page monolith. When you have dozens of developers touching the same codebase, release cycles slow down, regressions creep in everywhere and small UI changes suddenly need massive coordination. Microfrontends attack exactly that problem by slicing the UI into independently built and deployed pieces.

In this guide we will walk through what microfrontends are, why they appeared, how they relate to microservices, which core ideas you should respect when designing them, and how they work in practice with technologies like React, Angular, Next.js, Web Components, Webpack Module Federation and Single‑SPA. We will also dig into real architectural patterns, good practices, common pitfalls and concrete examples like a product catalog and cart implemented as separate microfrontends.

What are microfrontends and why did they appear?

The term “micro frontends” was popularized around 2016 in the ThoughtWorks Technology Radar as an answer to a very concrete trend: backend architectures were moving to microservices, but the browser side remained a big, brittle monolith owned by a single team. Over time that “fat client” SPA becomes hard to evolve, even though the backend is nicely split into small services.

A microfrontend is essentially the microservice idea applied to the browser UI: instead of one big frontend repository, you compose the user interface from multiple smaller, self‑contained applications. Each one owns a clear business domain (for example, “checkout”, “product search”, “student profiles”) and can be built, tested and deployed on its own cadence.

Just like microservices split backend logic into separate deployable units, microfrontends split the frontend into vertical slices that span from the database or APIs, through the backend, up to the user interface. A cross‑functional team owns that vertical slice end‑to‑end, from data schema to UI components.

This “vertical organization” contrasts with a horizontal split by layers (one team for UI, another for APIs, another for database). Vertical teams tend to ship faster because they do not need to coordinate every small change across half the company.

From an application point of view, a microfrontend is an autonomous web application that can be composed into a larger experience: it can have its own routing, state management, design system and deployment pipeline, as long as it respects a set of contracts with the rest of the system (URLs, events, APIs, shared libraries, etc.).

Core ideas and principles behind microfrontends

Several recurring principles show up in successful microfrontend architectures; they are not strict rules but they will save you from a lot of pain if you use them as guardrails.

Technology agnosticism is one of the most famous principles: each team should be able to choose and upgrade its tech stack without synchronizing with everyone else. Maybe one microfrontend is on React, another in Angular and a legacy one in Vue. Browser‑native abstractions like Custom Elements (Web Components) help hide those differences behind a standard DOM API.

Strong isolation between team codebases is another key goal. Ideally, microfrontends do not share a JavaScript runtime or global variables. Each bundle is self‑contained, loads its own dependencies and does not rely on hidden state from others. That reduces accidental coupling and makes independent deployments much more realistic.

To avoid naming conflicts, teams often agree on explicit namespaces for CSS classes, DOM events, localStorage keys, cookies or even Custom Element tag names. For example, a checkout team might prefix things with chk- or use a tag like <blue-buy>, while a recommendations team uses rec- or <green-recos>. At a glance, the DOM tells you who owns which piece.

Another principle is to prefer native browser capabilities over custom global APIs. Instead of inventing an all‑purpose PubSub system, you can rely on standard DOM events, CustomEvent, history API for routing or the DOM itself as integration layer. Whenever you really need a shared API, keep it as small and stable as possible.

Finally, resilience should be part of the design from day one. Each microfrontend should still deliver some value when JavaScript is slow, fails or is blocked. Techniques like server‑side rendering, progressive enhancement and skeleton screens help keep perceived performance high even under poor network conditions.

What is a “modern web application” in this context?

Not every website needs microfrontends or a complex browser integration strategy. A good mental model comes from the “documents‑to‑applications continuum”: on the left you have mostly static documents linked together; on the right you have fully interactive apps like an online photo editor.

If your project is closer to static documents, a simple server‑rendered composition is often enough. The server gathers HTML fragments from different sources, stitches them together and sends them to the browser. Updates happen via full page loads or small Ajax injections, and that’s perfectly fine.

When you move toward more dynamic, app‑like experiences—instant feedback, working offline, optimistic UI updates—pure server‑side integration stops being sufficient. You need client‑side composition, state management and routing. That’s where microfrontends become interesting: they give you a way to scale that complexity across many teams.

Vertical organization and domain‑driven slices

A common recommendation is to align microfrontends with business domains instead of technical layers. Think in terms of user journeys: “cart”, “product details”, “admin users”, “course schedules”, “student records”, “invoices”, etc.

Each of these domains can become its own microfrontend with a well‑defined responsibility. In a university system, one app could manage student profiles, another staff profiles, a third one course timetables and another the exam results UI. They share a shell and maybe some styling, but each is an independent application from a deployment perspective.

Good slicing also considers bounded contexts of your backend microservices. Ideally, the microfrontend for “billing” talks mainly to the billing microservices, the “catalog” frontend talks to catalog services, and so on. That keeps each vertical slice cohesive and reduces cross‑team dependencies.

Technical integration: DOM as the API and Web Components

In browser‑side microfrontend architectures, the DOM itself often acts as the primary integration API. Instead of calling each other’s JavaScript directly, teams expose functionality through HTML elements, attributes and events.

Custom Elements (part of Web Components) are a powerful primitive for this. A team can build a feature using whichever framework it wants and then wrap it as a custom tag, for example <order-minicart></order-minicart>. The public contract of that component is defined by its tag name, attributes and emitted events, not by its internal implementation.

Browser support for Custom Elements v1 is now solid across all major browsers, which means you rarely need polyfills. Most mainstream frameworks – React, Vue, Angular, Svelte, Preact – can render or embed custom elements as if they were regular HTML tags, and many of them can also be compiled into a custom element themselves.

The integration pattern looks something like this: a “product page” microfrontend decides which features appear on the page (selectors, buy buttons, mini‑cart, recommendations). It injects custom elements such as <blue-buy sku="t_porsche"></blue-buy> or <green-recos sku="t_porsche"></green-recos>. The teams owning those features register their elements with customElements.define and implement lifecycle callbacks like connectedCallback or attributeChangedCallback.

When a product variant changes, the page can either re‑create the element or just update its attributes. If the component observes relevant attributes, it can re‑render itself. Internally, the component might use template strings, React, Vue, or any rendering engine; the integrator does not need to care.

Client‑side communication: events and DOM relationships

Passing attributes works well for one‑way “data in” scenarios, but many real interactions require components to talk back to their environment or to siblings. A typical example is a “buy” button that must notify the mini‑cart when an item is added.

Instead of building a custom global event bus, you can lean on browser events. Components dispatch CustomEvent instances that bubble up through the DOM tree. A parent element or even window can listen for those events and orchestrate responses.

For example, the buy button might emit an event like blue:basket:changed with the current cart payload. The mini‑cart subscribes to that event on window or on a shared container element and refreshes its internal state whenever it fires.

This approach keeps components independent: the buy button has no idea who, if anyone, listens for its events. It just fulfills its contract. And the mini‑cart only depends on event semantics, not on the implementation details of other fragments.

Server‑side rendering and universal components

If you care about first‑paint performance, SEO or resilience when JavaScript fails, server‑side rendering (SSR) is essential. Pure client‑side Web Components only appear after the JS bundle downloads and runs, which may mean a white screen on slow networks.

A pragmatic solution is to combine Custom Elements with server‑side includes (SSI/ESI). Each microfrontend exposes an HTTP endpoint that returns the HTML for its fragment, for example /blue-buy?sku=t_porsche. The main page, rendered by a shell or host application, includes placeholders like <!--#include virtual="/blue-buy?sku=t_porsche" --> which the web server (often nginx) expands before sending the response to the browser.

At runtime in the browser, the same custom element is hydrated or re‑initialized once its JS bundle loads. That gives you a “universal” component: it can render on the server for speed and SEO, then behave like a fully interactive custom element on the client.

One drawback of SSR with includes is that the slowest fragment dictates total response time. Caching at the fragment level is almost mandatory. For expensive, highly personalized pieces (like recommendations) you may skip server‑side rendering and load them asynchronously on the client.

Skeleton screens are a good compromise to avoid jarring layout shifts. A fragment can server‑render a greyed‑out placeholder with approximately the same size as the final content. When the real data arrives client‑side, the skeleton is swapped out without big reflows.

Data loading and perceived performance

In a microfrontend world you have to think carefully about where and when data is fetched. You can fetch everything server‑side, everything client‑side, or use a hybrid. Each choice affects caching strategies, time‑to‑interactive and perceived speed.

Server‑side includes naturally encourage server‑side fetches per fragment. Each microfrontend talks to “its” backend services, renders HTML and returns it to the shell. That HTML can be cached independently of other fragments, which helps scale high‑traffic parts like login or product listings.

When loading data on the client you should budget for progressive states: initial skeleton, fast updates when attributes change, and fallback behavior when APIs are slow. Sometimes keeping old data in place until fresh data arrives is less visually jarring than flashing a skeleton for every minor change.

Microfrontends with React

React is a very popular choice for implementing microfrontends because of its ecosystem and rendering optimizations. The virtual DOM and diffing make it straightforward to update small parts of the UI based on prop changes or global state, and you can bundle React apps either as standalone SPAs or as Custom Elements.

Migration between React versions tends to be incremental and relatively painless compared to some other frameworks, which is helpful when many independent teams maintain separate microfrontends. You do not need all fragments to jump from one major version to another at the same time.

The flip side is that decentralized React microfrontends can create resource sprawl: multiple teams, multiple CI/CD pipelines, many bundles, many small repos. Without enough automation for build, provisioning and observability, that overhead becomes hard to manage.

Another practical issue is bundle size. If every microfrontend ships its own copy of React and shared libraries, total download size can explode, especially when several fragments are needed to render a page. Solutions like Module Federation (to share dependencies at runtime) or a strongly aligned stack across teams can mitigate this.

Microfrontends with Angular

Angular lends itself nicely to more opinionated microfrontend setups, particularly when you use monorepos and the tooling around them (like Nx). Angular workspaces are organized into projects and libraries, which makes it natural to split a large solution into multiple apps and shared libraries.

Since Angular 12 and Webpack 5, Module Federation has become a first‑class citizen. An Angular project can be configured as a host or a remote using schematic commands, wiring up the necessary webpack.config.js and bootstrap logic for you.

In this model, the “host” Angular app acts as the shell that orchestrates navigation, shared state and dependency sharing. Individual Angular microfrontends (remotes) expose Angular modules that the host can lazy‑load dynamically via Module Federation.

The usual Angular routing primitives still apply. Within a microfrontend, you use RouterModule.forChild for child route definitions so that the host is the only one using forRoot. That way, multiple Angular apps can coexist under a unified URL space without router conflicts.

Module Federation in practice (Angular example)

Webpack Module Federation is a Webpack 5 feature that allows multiple builds to share code at runtime. One build (the host) dynamically loads modules exposed by other builds (remotes) via a small manifest file, typically named remoteEntry.js.

In Angular you can scaffold this quite quickly. For example, you might create a host app (host-app) and then run a schematic like ng add @angular-architects/module-federation --project host-app --port 4200. This sets up a ModuleFederationPlugin configuration, bootstrapping files and runtime logic.

You then create two remote Angular apps: one for a product catalog and one for a shopping cart. Each app gets its own port (e.g. 4201 for products-app, 4202 for cart-app) and its own Module Federation config. In webpack.config.js of each remote you use exposes to publish a module (typically the main app module) under a key like ./ProductsModule or ./CartModule.

The host shell then defines routes that lazily load those remote modules via loadRemoteModule from @angular-architects/module-federation. For instance, navigating to /products triggers a dynamic import from http://localhost:4201/remoteEntry.js and loads ProductsModule; /cart does the same with the cart remote.

Inside the catalog microfrontend you might have a ProductsComponent that renders a table of items, reading data from a PRODUCTS_CATALOG constant and offering an “Add to cart” button. On click, it persists the item in localStorage under a “cart” key, incrementing quantities when the product already exists.

The cart microfrontend then reads from the same localStorage key, displays a table with product name, price, quantity and total, and offers a “Clear cart” button that wipes the storage and resets its internal state. This is a simple but illustrative way of sharing state across two independent apps without tight coupling.

Building the host shell: layout, home and navigation

A solid host shell is critical for a good user experience across microfrontends. It usually owns the global layout (header, footer, sidebars), top‑level routing and sometimes global state like authentication or feature flags.

In the Angular example, the host defines a LayoutComponent that renders a header and a nested router-outlet. The header lives in its own HeaderModule and exposes navigation links to the home page, product listing and cart via Angular’s routerLink. Route paths can be centralized in an enum like RoutesPath to avoid magic strings.

The layout routing module sets up a parent route with LayoutComponent as its component and defines child routes for /home, /products and /cart. The /home path loads a local HomeModule; the others use loadRemoteModule to pull in the Angular microfrontends at run time.

Within the host, a SharedModule can gather reusable building blocks like header, layout, common directives and constants. This module can be imported into the root AppModule along with AppRoutingModule, which points the empty path to the layout routing configuration.

Next.js and microfrontends

Next.js, as the production‑grade React framework, also plays well with a microfrontend approach, especially since it adopted Webpack 5 and therefore Module Federation support. Its focus on SSR, incremental static regeneration and routing makes it a good candidate for the shell, individual microfrontends, or both.

To implement microfrontends in Next.js you typically configure Module Federation at the Webpack level, exposing certain pages or components as remotes and consuming them from a host. Even though Module Federation is “just” a JavaScript bundler feature, you can think of it as an architectural pattern: it lets you dynamically load code owned by different teams without bundling everything together ahead of time.

For legacy Next.js versions without Webpack 5 you would need external adapters to enable federation support. From Next 10.2 onward, Webpack 5 support is built‑in, which simplifies the setup dramatically.

Single‑SPA and other microfrontend frameworks

Single‑SPA is another well‑known solution for microfrontends, particularly in React ecosystems. It focuses on orchestrating multiple independent apps on the same page, each mounted into its own DOM node based on the current route.

With Single‑SPA you can have several React applications (or even mix React, Vue, Angular) coexisting. The framework handles when to bootstrap, mount or unmount each microfrontend as the user navigates, and you wire it into your routing strategy (for example with React Router in each fragment).

When choosing between Single‑SPA and Module Federation, teams often consider their bundler/tooling preferences. Module Federation integrates deeply with Webpack (and, increasingly, with alternatives like Rspack or Rollup as they add support). Single‑SPA, on the other hand, is more about runtime orchestration than bundle sharing, so you can use it with different build tools while handling code sharing in other ways.

Objectives, features and benefits of microfrontends

The main objective of microfrontends is to scale frontend development across many teams without collapsing under coordination overhead. Instead of one giant codebase with synchronized releases, you get smaller independently deployable units.

Key objectives usually include enabling multiple teams to work in parallel, supporting independent deployment for different parts of the UI, maintaining flexibility to use different technologies where it makes sense and improving maintainability by reducing the size and complexity of each codebase.

Typical characteristics of such an architecture are component reuse through shared libraries, modular integration via an application shell, independent pipelines per microfrontend, performance optimization via CDNs and caching, centralized security handling and strong observability.

From a business perspective, the benefits are substantial: development scales better with more teams, failures are better isolated, features can be rolled out or rolled back per domain, and legacy frontend stacks can be migrated gradually by replacing one slice at a time rather than rewriting the whole app.

Key components in a microfrontend architecture

While implementations vary, most microfrontend architectures share a few structural components that keep everything coherent.

The application shell (or container) is the backbone. It owns the outer layout, global navigation, authentication, sometimes global state, and the logic for loading or unloading microfrontends. In browser‑based setups, it is the page that pulls in all other bundles.

Each microfrontend is a self‑contained module that implements a specific capability, such as product catalogue, shopping cart, user profile or admin panel. It exposes an integration surface (routes, Custom Elements, Module Federation remotes) and hides internal details from the rest of the system.

An event bus or message system is often present for cross‑microfrontend communication. This can be a simple abstraction over DOM events, a centralized Redux store, or a custom message broker. The goal is decoupled publish/subscribe semantics: a microfrontend emits events without knowing who handles them.

Shared libraries host reusable UI components, utilities, design tokens and common clients. In monorepo setups tools like Nx shine here, letting you define shared packages consumed by multiple apps with enforced boundaries and consistent versions.

Observability collectors and tooling (for example using OpenTelemetry) aggregate logs, metrics and traces from all microfrontends, making it possible to diagnose issues that span multiple fragments or services.

CDNs complete the picture on the delivery side, caching static assets like JS bundles, CSS and images close to users, reducing latency and offloading origin servers. In microfrontend setups, you often host each fragment’s assets on its own CDN path for independent caching and rollouts.

Architecture and design patterns for microfrontends

There is a rich catalog of patterns that apply specifically to microfrontends, usually defined by how you compose, deploy and connect them.

Component‑based composition means building the UI out of web components or similar primitives. Each component has a single responsibility, clear inputs and outputs, and can be tested in isolation. A higher‑level composition layer (like a shell or orchestration framework) arranges those components into full pages.

The federation pattern emphasizes complete application autonomy. Each microfrontend is a standalone app with its own lifecycle and team; the shell or an API gateway simply routes requests/data to the correct fragment. Communication happens through well‑defined REST APIs or events.

The application shell pattern is effectively the “host” approach. The shell loads microfrontends, handles cross‑cutting concerns like navigation and security and ensures a consistent look & feel. This is very common with Module Federation or Single‑SPA based setups.

API gateway and Backend‑for‑Frontend (BFF) patterns focus on the server side. An API gateway sits in front of many backend services, routing requests and applying security. A BFF goes further: each frontend (web, mobile, IoT) may get its own dedicated backend tailored to its needs.

Distributed data storage patterns acknowledge that different microfrontends might manage their own data sources or caches. In the browser this often means using separate localStorage keys, IndexedDB databases or in‑memory stores, while on the backend it means separate databases per microservice.

Observability, independent deployment, horizontal scalability and security patterns address operational concerns: how to monitor each fragment, how to deploy them without global outages, how to scale them under load and how to enforce authentication/authorization consistently.

Routing composition and lazy loading patterns are central to UX and performance. A master router decides which microfrontend handles which path, and each microfrontend has its own internal router. Lazy loading ensures that you only download the code for the fragments actually needed for the current route.

Finally, event‑based communication patterns ensure that loosely coupled microfrontends can still coordinate through domain events, without introducing direct dependencies that would defeat the point of modularity.

When to use microfrontends (and when not)

Microfrontends shine in large, complex applications with well‑defined functional domains. Think e‑commerce platforms, enterprise management systems, municipal portals, education platforms, large health portals or any product with many teams working on separate functional areas.

They are particularly useful when you have multiple teams working in parallel that need autonomy in tech choices, release cycles and priorities, or when you are slowly modernizing a legacy frontend and cannot afford a full rewrite. You can carve out one area at a time into a new microfrontend and integrate it with the old shell.

They also help when different parts of the app need to scale differently. A login or checkout page might get much more traffic than an admin configuration screen; scaling those slices independently can save a lot of infrastructure cost.

However, microfrontends are not a free lunch. They add architectural complexity, require strong coordination on UX and shared contracts, and introduce new failure modes (for example, one fragment not loading). For small or medium apps with a single team, a well‑structured monolith is often simpler and more cost‑effective.

Teams should also be wary of “framework anarchy”. While it is technically possible for each microfrontend to use a completely different stack, an uncontrolled mix of frameworks makes hiring, tooling and code sharing harder. A reasonable level of alignment (for example “we are a React‑first shop, but we allow Angular for specific domains”) usually works better in the long run.

Microfrontends extend the microservices mindset to the browser, giving teams a way to slice big frontends into autonomous, domain‑oriented pieces that can evolve, deploy and scale independently while still forming a cohesive user experience when combined through an application shell, shared libraries, Module Federation, Web Components or orchestration frameworks like Single‑SPA.

Related posts: