What Is Micro-Frontend Architecture? From a Frontend Monolith to Independently Deployable Applications
Learn the fundamentals of micro-frontend architecture, including why it exists, how it works, common integration approaches, its relationship with Module Federation, monorepos, and Docker, as well as the trade-offs you should consider.
On this page
- Starting with a frontend monolith
- What does micro-frontend architecture change?
- Is splitting an application into components considered micro-frontend architecture?
- Shell, host, and remote applications
- Common micro-frontend integration approaches
- Who should own routing?
- Sharing state between micro-frontends
- Authentication in a micro-frontend system
- How do you keep the user interface consistent?
- Independent deployment does not mean zero dependencies
- Micro-frontends, monorepos, and Turborepo
- Are micro-frontends related to Docker?
- Should different teams use different frameworks?
- At what level should you split micro-frontends?
- Key benefits
- Trade-offs you need to accept
- When should you use micro-frontends?
- When should you avoid micro-frontends?
- A practical adoption strategy
- Conclusion
What Is Micro-Frontend Architecture? From a Frontend Monolith to Independently Deployable Applications
When starting a frontend project, we usually need only one repository, one React or Vue application, and one deployment pipeline. This approach is simple, easy to develop, and suitable for most projects.
But what happens when the application grows to contain dozens of modules, multiple teams work on it simultaneously, and every small change requires the entire frontend to be built, tested, and deployed again?
This is where micro-frontend architecture comes into the picture.
Micro-frontend is not a framework or library. It is an architectural approach that divides a large frontend application into smaller applications based on business domains. These applications can then be developed and deployed relatively independently.
In this article, we will explore:
- What problems micro-frontends are designed to solve.
- How a micro-frontend system works.
- What shell, host, and remote applications are.
- Different ways to combine multiple frontends into one product.
- The role of Module Federation.
- How micro-frontends relate to monorepos, Turborepo, and Docker.
- When you should and should not use this architecture.
Starting with a frontend monolith
Imagine that we are building an e-commerce platform with React:
src/
├── products/
├── cart/
├── checkout/
├── account/
├── promotions/
└── admin/
At first, this structure works well. Everything lives inside one application and shares the same router, state management solution, component library, and build pipeline.
As the product grows, the company starts organizing developers into teams based on business responsibilities:
| Team | Responsibility |
|---|---|
| Product team | Product listing and product details |
| Cart team | Shopping cart |
| Checkout team | Payment and checkout |
| Account team | User account management |
Although the business responsibilities have been separated, everyone still works in the same frontend repository and must deploy their changes together.
This can introduce several problems:
- Multiple teams frequently modify the same codebase.
- Build and test times become increasingly long.
- A small shopping cart change still requires the entire frontend to be deployed.
- Dependency upgrades must be coordinated across multiple teams.
- A bug in one module can affect the entire application.
- Code ownership becomes unclear.
This type of application is commonly called a frontend monolith, meaning the entire frontend is built and deployed as a single unit.
A frontend monolith is not inherently a bad architecture. In fact, it remains the best choice for most projects. Problems usually appear only when organizational scale and independent deployment requirements exceed what a single application can support efficiently.
What does micro-frontend architecture change?
Instead of dividing the codebase only into folders, micro-frontend architecture separates the system into applications with clearly defined boundaries:
E-commerce application
├── Product application
├── Cart application
├── Checkout application
└── Account application
Each smaller application can have:
- Its own owning team.
- Its own source code and testing pipeline.
- Its own release process.
- Its own deployment.
- A relatively independent development lifecycle.
Users still experience a single, unified website. The separation exists only within the underlying architecture.
The most important aspect of micro-frontend architecture is not simply dividing code into smaller pieces. Its primary purpose is to establish clear ownership and independent deployment based on business domains.
Is splitting an application into components considered micro-frontend architecture?
Consider the following example:
function App() {
return (
<>
<Product />
<Cart />
<Checkout />
</>
);
}
The application has been divided into multiple components. However, if all these components:
- Live in the same repository.
- Are built together.
- Use the same pipeline.
- Must be deployed together.
then the application is still a frontend monolith.
Similarly, moving code into separate npm packages does not necessarily create micro-frontends. If the host application must install a new package version, rebuild, and redeploy whenever that package changes, the applications still have limited independence.
Micro-frontend architecture exists on a spectrum rather than being an absolute standard. A system may have separate codebases but share one repository, or it may support independent deployments while sharing a design system.
The appropriate level of separation should depend on the real problems the organization needs to solve.
Shell, host, and remote applications
The central application in a micro-frontend system is commonly called the shell, host, or container application.
The shell is responsible for application-wide concerns such as:
- The shared layout.
- The header, sidebar, and footer.
- Top-level navigation.
- Authentication and current user information.
- Theme, locale, and feature flags.
- Loading micro-frontends when they are needed.
Applications loaded by the shell are commonly called remote applications.
Shell application
├── Header
├── Product remote
├── Cart remote
└── Account remote
In a development environment, these applications may run at different addresses:
Shell → http://localhost:3000
Product → http://localhost:3001
Cart → http://localhost:3002
Account → http://localhost:3003
When a user visits /cart, the shell loads the Cart application and displays it inside the shared layout. When the user navigates to /account, the shell switches to the Account application.
Common micro-frontend integration approaches
Micro-frontend is an architectural approach, so there are several technical ways to implement it. Module Federation is only one of them.
1. Build-time integration
Each business domain can be published as an npm package:
import { CartPage } from "@company/cart";
This approach is simple, provides good type safety, and works well with existing development tools. However, when the Cart package releases a new version, the shell must update the dependency, rebuild, and redeploy.
Build-time integration creates clear source code boundaries, but it does not provide fully independent deployments.
2. Runtime integration with JavaScript
The shell can load another application's bundle at runtime:
<script src="https://cart.example.com/cart.js"></script>
It can then mount the application into an element on the page:
window.CartApp.mount(document.getElementById("cart-root"));
The Cart team can deploy a new bundle without rebuilding the shell. In exchange, the team must manage application lifecycles, dependencies, loading failures, and integration contracts themselves.
3. Module Federation
Module Federation allows one JavaScript application to load modules from another application at runtime.
Inside the shell, using a remote component can look similar to a regular dynamic import:
const CartPage = React.lazy(() => import("cart/CartPage"));
However, CartPage is not included in the shell bundle. When the application runs, the browser downloads the module from the Cart application.
Browser
├── loads the shell bundle
├── loads cart/remoteEntry.js
└── loads the CartPage chunk
Module Federation supports:
- Declaring modules that an application exposes.
- Declaring remote applications that a host consumes.
- Loading modules at runtime.
- Sharing dependencies between hosts and remotes.
- Avoiding duplicate copies of libraries such as React and React DOM.
Webpack 5 popularized Module Federation through ModuleFederationPlugin. However, it is important to distinguish between the architecture and the implementation mechanism:
Micro-frontend is an architectural decision. Module Federation is a technical mechanism that can be used to implement that decision.
You can build micro-frontends without Module Federation. Likewise, using Module Federation does not automatically result in a well-designed micro-frontend architecture.
4. iframe integration
An application can be embedded using an iframe:
<iframe src="https://checkout.example.com"></iframe>
An iframe provides strong CSS and JavaScript isolation. It can be suitable for payment forms, third-party content, or legacy applications that need to remain isolated.
However, iframes make navigation, authentication, accessibility, and layout synchronization more difficult. Communication between the shell and iframe usually needs to use postMessage.
5. Web Components
A team can expose its micro-frontend as a custom element:
<product-list category="laptop"></product-list>
Web Components create boundaries that do not directly depend on React or Vue. This can be useful when different teams use different frameworks.
However, complex data transfer, server-side rendering, and shared state still require careful design.
6. Server-side composition
Instead of combining everything in the browser, a server or edge layer can retrieve HTML from multiple sources and assemble the final page:
Request
↓
Server or Edge
├── retrieves Header HTML
├── retrieves Product HTML
└── retrieves Recommendation HTML
↓
Combined HTML
This approach can provide good SEO and initial rendering performance. However, caching, timeouts, hydration, and error handling across multiple services become more complicated.
Who should own routing?
A common approach is to let the shell manage top-level routes:
<Routes>
<Route path="/products/*" element={<ProductApp />} />
<Route path="/cart/*" element={<CartApp />} />
<Route path="/account/*" element={<AccountApp />} />
</Routes>
Each remote then manages routes within its own domain. For example, the Product application might own:
/products
/products/:productId
/products/:productId/reviews
A useful rule is:
The shell owns application-level navigation, while each micro-frontend owns navigation within its business domain.
If every application tries to control the entire routing system, they may compete to update the browser history, define conflicting routes, or handle 404 pages inconsistently.
Sharing state between micro-frontends
State sharing is one of the most challenging parts of micro-frontend architecture.
Suppose the Product application, Cart application, and Header all use one large Redux store. Every application can read and modify the state of the others.
Although the code may have been divided into separate bundles, the applications remain tightly coupled at runtime.
A better approach is to classify state according to ownership.
Domain state
For example, the products in the shopping cart should be managed by the Cart application:
type CartState = {
items: CartItem[];
total: number;
};
The Product application should not modify the Cart application's internal store directly. Instead, it should send a command or call a public API such as addToCart.
Application-wide state
Some information genuinely needs to be shared across the entire system:
- The current user.
- Authentication status.
- Locale.
- Theme.
- Feature flags.
The shell can expose this information through a stable interface.
Event-based communication
Applications can also communicate through events:
eventBus.emit("cart:item-added", {
productId: "123",
quantity: 1,
});
The Header can listen for this event and update the number displayed on the shopping cart icon.
Events reduce the need for applications to import each other's code directly. However, excessive event usage makes data flow difficult to understand and debug.
Event names, payloads, and versions should be defined as explicit contracts, preferably with TypeScript support.
Authentication in a micro-frontend system
Within a unified product, the shell commonly manages authentication:
Shell
├── checks the session
├── handles token refreshing
├── provides the current user
└── mounts the appropriate remote
Remote applications should not implement completely different login flows if they belong to the same system.
If the server uses httpOnly cookies, the browser can automatically send the cookie to the appropriate domain. Remote applications do not need, and should not be able, to read the refresh token directly through JavaScript.
When applications run on multiple subdomains such as app.example.com, cart.example.com, and account.example.com, the team needs to carefully design:
- The cookie domain.
- CORS configuration.
SameSiteandSecuresettings.- CSRF protection.
- Session expiration.
- Redirects after authentication.
- Single sign-on.
How do you keep the user interface consistent?
Users do not care how many teams are responsible for building a page. They still expect the entire product to use consistent colors, typography, spacing, and interaction patterns.
For this reason, teams commonly share a design system:
@company/design-system
├── Button
├── Input
├── Modal
├── Typography
└── Design tokens
The design system should have versioning, migration guides, and visual regression tests.
If a component API introduces a breaking change without maintaining compatibility, multiple remote applications can fail at the same time.
Global CSS also introduces risks. For example, a remote may define:
button {
background: red;
}
This style could unintentionally modify every button in the application.
CSS Modules, scoped CSS, naming conventions, and design tokens can help reduce conflicts. Remote applications should also avoid introducing their own global CSS resets.
Independent deployment does not mean zero dependencies
Suppose the shell loads the Cart application from:
https://cart.example.com/remoteEntry.js
The Cart team can deploy a new version without releasing the shell. This is a major benefit, but it also introduces a risk: the code loaded by the shell today may be different from the code it loads tomorrow.
A production system should have:
- Contract tests between the host and remotes.
- Backward compatibility.
- Clear versioning.
- Canary deployments and rollbacks.
- Error monitoring.
- An appropriate caching strategy.
- Fallbacks when a remote cannot be loaded.
For example, a React application could use:
<ErrorBoundary fallback={<CartUnavailable />}>
<Suspense fallback={<CartSkeleton />}>
<CartPage />
</Suspense>
</ErrorBoundary>
If the Cart application fails, the rest of the website should continue to work.
Micro-frontends, monorepos, and Turborepo
Monorepos and micro-frontends answer two different questions:
- Monorepo: Where is the source code stored?
- Micro-frontend: How is the frontend divided, owned, integrated, and deployed?
A micro-frontend system can still live inside a monorepo:
apps/
├── shell/
├── product/
├── cart/
└── account/
packages/
├── design-system/
├── shared-types/
└── eslint-config/
Turborepo can help manage workspaces, cache builds, and run tasks within this structure. Each application can still have its own deployment pipeline.
Conversely, a monorepo does not automatically make a system a micro-frontend architecture. If every application is always built and deployed together, the system still lacks independent releases.
Are micro-frontends related to Docker?
Not directly.
| Concept | Responsibility |
|---|---|
| Micro-frontend | Organizing the frontend architecture |
| Module Federation | Loading and sharing JavaScript modules at runtime |
| Monorepo | Storing and managing source code |
| Turborepo | Managing tasks and caching in a monorepo |
| Docker | Packaging and running applications |
Each micro-frontend can be deployed with Docker, but it can also be deployed using Vercel, Netlify, S3 with a CDN, or a regular Nginx server.
Docker is an infrastructure and deployment option. It is not a requirement for building micro-frontends.
Should different teams use different frameworks?
Technically, you can build a system like this:
Shell → React
Product → Vue
Checkout → React
Legacy → Angular
This capability can be useful when gradually migrating a legacy system. However, when building a new product, using multiple frameworks usually increases bundle size, maintenance costs, design system complexity, and the amount of knowledge teams need to maintain.
Micro-frontend architecture allows teams to use different technologies, but that does not mean they should do so without a clear reason.
At what level should you split micro-frontends?
Page-level boundaries are usually a safe starting point:
/products → Product application
/cart → Cart application
/account → Account application
Section-level boundaries are more difficult because multiple remote applications must coordinate within the same page:
Product detail page
├── Product information
├── Reviews
└── Recommendations
Separating small components into applications such as Button application, Modal application, or Table application usually provides little value.
The networking, versioning, and integration costs are likely to be greater than the benefits of independent deployment.
A useful rule is:
Split the system by business capability, not by UI component type.
A Checkout application represents a business boundary. A Button application does not.
Key benefits
When applied to the right problem, micro-frontend architecture provides:
- Clear ownership based on business domains.
- Independent development and deployment for different teams.
- A smaller impact area for changes.
- The ability to upgrade individual parts gradually.
- Incremental migration of legacy applications.
- Different release cycles for different domains.
- Better failure isolation when designed correctly.
Most of these benefits relate to scaling teams and development processes, not simply organizing code.
Trade-offs you need to accept
Micro-frontend architecture does not eliminate complexity. It moves part of that complexity from the codebase into integration and operations.
Teams will need to handle:
- Duplicate dependencies.
- Larger bundle sizes.
- CSS conflicts.
- More complicated routing and state sharing.
- Authentication across multiple domains.
- Inconsistent user interfaces.
- Version mismatches between hosts and remotes.
- Remotes that fail or cannot be loaded.
- Local development that requires multiple applications.
- More difficult end-to-end testing and debugging.
- Incorrect caching that serves outdated bundles.
Micro-frontends provide real value only when the problems caused by team size and deployment coordination are significant enough to justify these technical costs.
When should you use micro-frontends?
Micro-frontend architecture can be appropriate when:
- The product is large and has clearly defined business domains.
- Multiple frontend teams work on the product simultaneously.
- Each team needs its own release schedule.
- The frontend monolith is slowing down delivery.
- Team ownership is already clearly defined.
- The company needs to migrate a legacy system incrementally.
- CI/CD, automated testing, and monitoring are sufficiently mature.
For example, a product with 20 frontend developers divided into Product, Checkout, Account, and Operations teams may benefit from this architecture.
When should you avoid micro-frontends?
Micro-frontends are often unnecessary when:
- The project is small or medium-sized.
- There is only one frontend team.
- Modules frequently change together.
- Independent deployment is not required.
- Business boundaries are unclear.
- CI/CD and automated testing are not yet stable.
- The current frontend monolith is not causing real problems.
In these situations, a modular monolith is usually a better choice:
src/
├── modules/
│ ├── product/
│ ├── cart/
│ └── account/
├── shared/
└── app/
You can still establish clear boundaries, limit dependencies, and define ownership without taking on the additional complexity of runtime integration.
A practical adoption strategy
If your system begins to encounter scaling problems, you do not need to split the entire frontend immediately.
A practical adoption strategy might look like this:
- Organize the frontend monolith by business domain.
- Limit dependencies between domains.
- Identify domains with clear ownership and release cycles.
- Extract only the domain that genuinely needs independent deployment.
- Start with page-level boundaries.
- Let the shell manage top-level routing and authentication.
- Establish a stable design system and integration contracts.
- Add error boundaries, fallbacks, and monitoring.
- Test the approach with one domain before expanding it.
This approach allows the team to verify whether micro-frontends actually solve the problem or simply introduce another layer of complexity.
Conclusion
Micro-frontend is an architectural approach that divides a large frontend into smaller applications based on business domains. Its primary goal is to allow multiple teams to own, develop, and deploy different parts of a product relatively independently.
Here are the key points to remember:
- Splitting an application into components does not make it a micro-frontend architecture.
- Micro-frontend is an architectural approach, while Module Federation is an implementation tool.
- Monorepos and Turborepo can help organize source code, but they do not determine whether a system uses micro-frontends.
- Docker is related only to how applications are packaged and deployed.
- Boundaries should follow business domains rather than small UI components.
- Independent deployment introduces additional costs involving contracts, versioning, monitoring, and failure handling.
- For most small projects, a modular monolith remains the simpler and more effective choice.
Micro-frontend architecture should not be the default starting point for every product. It is an architectural tool designed for a specific set of problems: situations where organizational scale, ownership, and independent deployment requirements have become genuine limitations of the frontend monolith.
