The whole system on one page. Keep it open; we'll walk through every box.
Picture a typical web shop. One team owns the product catalog, another owns orders, a third owns user profiles. In the beginning, everyone works in one frontend and one backend. It's simple, and that's fine.
Then the company grows. Ten developers become fifty. Every release needs everyone to agree. A small bug on the profile page blocks the orders team from shipping. A routine library upgrade turns into a three-week, all-hands project.
This is the problem microfrontends and microservices solve: each team builds, tests and deploys its own part of the product, on its own schedule, while users still see one seamless application.
In this article we'll look at an architecture that does exactly that: how it works, why each piece exists, and above all how deployment works. The headline is this: when one part of the UI changes, you deploy only that part, and the main application doesn't need to be touched at all.
A complete, runnable example is on GitHub: github.com/it-nilesh/slice-platform-starter (React + Module Federation on the frontend, .NET 10 microservices on the backend, NGINX in front).
it-nilesh
/
slice-platform-starter
Production-shaped scaffolding for microfrontends + .NET 10 microservices behind an NGINX gateway: runtime Module Federation, a slice CLI, and independent CI/CD per slice.
MicroFrontend PoC
A small, production-shaped proof of concept for microfrontends + microservices behind a single NGINX gateway, packaged as a framework:
-
Manifest-driven dynamic remotes: adding a microfrontend never requires rebuilding or redeploying the shell.
-
Vertical slices (one API + one MFE + one pipeline per team), generated and managed by the
./sliceCLI. -
Independent CI/CD per slice: test, scan, sign and push only what changed; release each slice with its own tags.
-
Frontend: React 19 + Vite 8 + Module Federation (
@module-federation/vite+@module-federation/runtime), TypeScript -
Backend: .NET 10 Minimal APIs, one service per business capability
-
Gateway: NGINX, the only public entry point
Architecture
The diagram above shows the moving parts. In text form:
Browser (http://localhost:8080)
│
┌───────────────▼────────────────┐
│ gateway (NGINX) │ allowlist (deny by default), rate limiting
│ /config/mfe-manifest.json │ security headers + CSP, request-id, JSON logs
└──┬──────────────┬───────────┬──┘
/ (host) │ /mfe/<name>/│ │ /api/<name>/…When do you need this architecture?
Let's answer the most important question first, because the honest answer is: most applications don't.
Microfrontends and microservices solve an organizational problem, not a technical one. They're worth it when:
- Several teams work on the same product, and each owns a clear business area (catalog, orders, payments, profile…).
- Teams need to release independently, several times a week, without coordinating a "big release day" with everyone else.
- One area's problems must not block or break the others.
- Parts of the product change at very different speeds. A checkout page might change daily while the profile page changes once a quarter.
They're probably not worth it when:
- You're one or two teams. A well-structured modular monolith (one application with clean internal boundaries) is simpler, faster to build and easier to debug.
- The product is small or early-stage, and boundaries between areas are still changing.
- You don't yet have basic automation: CI pipelines, containers, monitoring. This architecture multiplies the number of things to deploy, so manual processes don't scale.
The costs are real: more moving parts, network calls that can fail, versions that drift apart, and debugging across several services. Choose this architecture when the cost of teams waiting for each other is higher than the cost of that extra complexity.
The big idea, explained with a shopping mall
Before any technical detail, let's build a mental model. Think of the application as a shopping mall:
- The mall building is the shell (also called the host). It has the entrance, the hallways and the signs. It doesn't sell anything itself.
- Each shop is a microfrontend: the Catalog shop, the Orders shop, the Profile shop. Each is run by a different owner, who redecorates and restocks it whenever they like.
- Each shop's back office and stockroom is a microservice. The Orders shop has its own stockroom (the Orders API) that only it manages.
- The security desk at the entrance is the gateway. Every visitor passes through it, and it knows which hallway leads where.
- The directory board at the entrance is the manifest. It lists which shops are open today and where to find them.
Now the key insight, and the reason this architecture exists:
- When a shop redecorates, the mall building isn't touched.
- When a new shop opens, nobody rebuilds the building. Someone adds a line to the directory board.
- When a shop closes for repairs, the rest of the mall stays open.
That's exactly how deployment works in this architecture. Keep this picture in mind; everything else follows from it.
Seven terms you need
Microfrontend (MFE): a small, independently deployed UI application that owns one part of the product, such as the Orders page.
Shell (host): the outer application. It draws the header and navigation, and loads the right microfrontend for each page. It contains no business features itself.
Microservice: a small, independently deployed backend that owns one business capability and its data, such as the Orders API.
Gateway: the single entry point for all traffic. Every browser request goes to it, and it forwards each one to the right container.
Manifest: a small configuration file listing which microfrontends exist, where to download them and which page they belong to. The shell reads it at runtime.
Module Federation: the browser technology that lets the shell load a microfrontend's code at runtime, even though the shell was never built with it.
Vertical slice: one business feature end to end (its microfrontend, its microservice and its own pipeline), owned by one team. The example has three: Catalog, Orders and Profile.
The architecture, piece by piece
The gateway: one front door
Every request from the browser goes to a single address: the gateway (NGINX in the example). It routes by a simple naming convention:
-
/goes to the shell -
/mfe/orders/goes to the Orders microfrontend -
/api/orders/goes to the Orders API
Why it matters:
- One address for everything means the browser never talks to several servers, so there's no cross-origin (CORS) configuration to maintain, and strict browser security rules are easy to apply.
- Security in one place: security headers, rate limiting and request size limits are applied once, for every team.
- Safe by default: only services on an allowlist are reachable. A new internal service can't become public by accident.
The shell: a frame that knows nothing about its content
The shell draws the frame (header, navigation, error handling) and loads microfrontends into it. The crucial design decision: the shell's code contains no list of microfrontends. It reads them from the manifest every time the app starts:
{
"schemaVersion": 1,
"remotes": [
{ "name": "catalog", "entry": "/mfe/catalog/remoteEntry.js", "route": "/catalog", "nav": { "label": "Catalog" } },
{ "name": "orders", "entry": "/mfe/orders/remoteEntry.js", "route": "/orders", "nav": { "label": "Orders" } }
]
}
From this file alone, the shell knows which pages exist, which menu items to show and where to download each microfrontend.
Why it matters: many microfrontend tutorials hard-code the list of microfrontends into the shell's build. Then every new microfrontend forces a shell rebuild and redeploy, which quietly brings back the very coupling we were trying to remove. Reading the list at runtime is what makes independent deployment real.
Because the manifest decides which code runs in the browser, the shell also validates every entry. A microfrontend may only be loaded from the same website, under its own folder, so a bad or malicious entry can't point the app at someone else's script.
The microfrontends: independent apps that share a page
Each microfrontend is a normal React application with its own folder, its own build, its own container and its own pipeline. It exposes one component (its page) for the shell to load.
A few rules keep them truly independent:
- They never import each other's code. If Catalog imported from Orders, they could no longer be deployed separately.
- They communicate through a small, shared contract. When you add a product to the cart in Catalog, it announces a "cart changed" event. The shell (cart badge) and Orders (cart list) listen for it. Nobody knows who else is listening.
- Shared libraries are loaded once. React is marked as shared, so the page loads a single copy instead of one per microfrontend.
- Each one can run on its own, without the shell, so a team can develop and test its page in isolation.
The Catalog page is one application; the header with the cart badge is another (the shell). The user can't tell.
The microservices: each team owns its backend and its data
Each microfrontend talks to its own API: Catalog to the Catalog API, Orders to the Orders API. Each service owns its data, and no other service touches its database.
When services need each other, they talk directly over the private network, not through the browser. For example, when you place an order, the Orders API asks the Catalog API for the real prices.
Why it matters: this follows a rule every system should follow: the browser sends intent, the server decides facts. The browser says which products and how many; the prices come from the service that owns them. Anything the browser sends can be changed by the user, so nothing important is trusted from it.
All services also share a small common building block for health checks, logging and error responses, so every team gets the same production basics without rebuilding them.
How it works: follow one click
Let's follow what happens when a user opens https://shop.example/orders:
- The browser asks the gateway for the page. The gateway forwards it to the shell, which returns a small HTML page and its JavaScript.
-
The shell reads the manifest. It learns that "Orders lives at
/mfe/orders/remoteEntry.jsand belongs on/orders", and builds the navigation menu. - The shell loads the Orders microfrontend. It downloads Orders' entry file only now, because the user is on the Orders page. Pages nobody visits are never downloaded.
-
The Orders microfrontend calls its API. It requests
/api/orders, which the gateway forwards to the Orders API. - When the user places an order, the Orders API asks the Catalog API for the real prices, over the private network.
Notice what didn't happen: the shell never needed to know anything about Orders in advance. It learned everything from the manifest, at runtime. That's the property that makes independent deployment possible.
Designed for failure
In a system with many independent parts, something is always being deployed, restarting or briefly down. The architecture makes sure one broken part never takes down the whole page:
- Each microfrontend is isolated. If Catalog fails to load, only the Catalog area shows "unavailable". The header, the navigation and every other page keep working.
- Loading has a timeout, so a slow server never means an endless spinner.
- "Try again" recovers without reloading the whole page once the service is back.
- The gateway returns a clear "service unavailable" for a slice that's down, instead of a confusing error.
The Catalog container is stopped. Only its area shows an error; navigation and the other pages still work.
Deployment: ship only what changed
This is where the architecture pays off. Let's start with the headline.
Changing a microfrontend? Deploy only that microfrontend
Say the Orders team redesigns the order history table. Here's the entire deployment:
- The Orders team merges its change.
-
Only the Orders pipeline runs. It builds, tests and publishes a new
mfe-ordersimage. - Only the
mfe-orderscontainer is replaced. - Users get the new Orders page on their next page load.
What wasn't touched:
- The shell: not rebuilt, not redeployed, not restarted.
- Catalog and Profile: not rebuilt, not redeployed.
- The gateway: not changed.
- The manifest: not changed. Orders still lives at the same address.
No other team had to be asked, wait, or re-test anything.
Why the shell never needs updating
Three design choices make this work:
1. The shell loads microfrontends at runtime, not at build time. The shell's build contains no microfrontend code at all. It downloads each one in the browser, from the address in the manifest. A new Orders version is simply what that address returns now.
2. A stable entry file plus versioned content files. Each microfrontend has one small entry file with a fixed name, remoteEntry.js. The browser is told to always check it for updates. That file points to the real code, whose file names include a unique version fingerprint (like App-3f9a1c.js), so those can be cached forever. After a deploy, the entry file points to new fingerprinted files, and every browser picks them up on its next load.
3. A contract instead of shared code. The shell and the microfrontends agree only on a small, stable contract: "expose a page component", "announce cart changes with this event". As long as a new version keeps that contract, the shell doesn't care what changed inside.
Every kind of change, and what it costs to ship
- Change a microfrontend's UI: deploy that one microfrontend. Shell untouched.
- Change a microservice: deploy that one service. Shell and microfrontends untouched.
- Add a brand-new microfrontend: deploy its containers, then add one entry to the manifest and one line to the gateway's allowlist. Shell untouched: it discovers the new page on the next load.
-
Turn off a broken microfrontend: set
"enabled": falsein the manifest. It disappears from the app on the next page load, with no deployment at all. - Change the shell itself (for example the header design): deploy the shell. This is the only case that touches it, and it's rare, because the shell contains no business features.
A fourth microfrontend, "Inventory", added by deploying it and adding one manifest entry. The shell was never rebuilt or restarted.
Independent pipelines
Independent deployment only works if the pipelines are independent too. Each slice has its own CI/CD pipeline that runs only when that slice's files change:
- A change in the Orders microfrontend builds and publishes only the Orders microfrontend image.
- A change in the Orders API builds and publishes only the Orders API image.
- A release tag like
orders-v1.4.0releases only the Orders slice.
Every pipeline produces versioned, immutable images (for example mfe-orders:sha-4e7fae5 or mfe-orders:1.4.0). "Immutable" means a tag always refers to exactly the same code, which makes every deployment predictable and every rollback trivial.
Where it runs
The deployment unit is always one container. That fits any modern platform:
-
A single server with Docker Compose: replace one service at a time (
docker compose up -d --no-deps mfe-orders) while everything else keeps running. - Kubernetes: each container becomes its own Deployment and Service, and the manifest and allowlist become configuration (ConfigMaps), so changing them never requires a new image.
- GitOps (Argo CD or Flux): a separate repository records which version runs in each environment. Deploying or promoting a slice becomes a small, reviewed pull request that changes one version number.
Deploying safely
A few habits keep independent deployments safe:
- Deploy the API before the UI, and keep API changes backward compatible (add fields, don't remove them). For a short time, old and new versions run side by side, so each must work with the other.
- Treat the shared contract like a public API: only ever add to it.
- Roll back by redeploying the previous version tag. Because tags are immutable, you know exactly what you're getting back.
- Use the kill switch when you need a broken page gone now: hide it in the manifest, then fix it calmly.
Why this matters
Put together, this architecture changes how a growing organization works:
- Teams move at their own speed. The Orders team can ship five times today without asking anyone.
- Releases get smaller and safer. Deploying one small piece is less risky than deploying everything, and easier to roll back.
- Failures stay contained, both at runtime (one broken page) and at deploy time (one bad release).
- New features don't require touching the core. Adding a whole new area of the product is a deployment plus a configuration change.
- Ownership is clear. Each team owns its slice end to end, from the database to the screen to the pipeline.
The price is more moving parts. That's exactly why it pays off for many teams working on one product, and why a single small team is usually better served by a modular monolith.
Wrapping up
The whole architecture comes down to one idea, the shopping mall: the shell is the building, not a shop. It provides the entrance, the hallways and the directory board, but it knows nothing about what each shop sells. Because of that:
- A shop can redecorate (deploy a new microfrontend version) without touching the building.
- A new shop can open by adding a line to the directory board (the manifest).
- A shop can close for repairs (fail, or be switched off) while the rest of the mall stays open.
If your teams keep waiting on each other to release, this is the architecture to reach for.
The complete, runnable example is on GitHub: github.com/it-nilesh/slice-platform-starter. Clone it, run docker compose up -d --build, open the app, then change one microfrontend and redeploy just that one. Watching the shell stay untouched is the best way to make the idea click.
If this helped you understand microfrontends, leave a reaction and follow for more architecture deep dives. Questions or a different approach? Drop them in the comments.





Top comments (0)