What you'll learn: By the end of this lesson you will be able to define a front-end monolith, a modular monolith and a micro-frontend architecture, name the benefits the idea promises and the three costs its best-known description warns about, decide whether a given front end should be split by looking at team ownership and release coupling rather than code size, pick seams that follow business slices instead of technical layers, and recognise a "distributed front-end monolith" before it ships.
Think of a shopping mall with a single kitchen that cooks for every restaurant in the building. Every menu change has to be agreed with the kitchen, and one burnt batch ruins dinner for everybody. Now think of a food court: each stall has its own kitchen and its own opening hours, and the mall supplies the tables, the entrance and the cleaning. Stalls move faster, but the mall has a new job: keeping the stalls from blocking each other's queues, from using the same fryer, or from playing music on top of each other.
A front-end monolith is the mall with one kitchen. A micro-frontend architecture is the food court. The running example for this lesson is an online grocery storefront with four areas: a product Catalogue, a Cart and Checkout, an Account area with order history, and a shared header and footer. At first one team builds all of it. Later several teams do. The question is what to change, and when.
The core idea
A micro-frontend split is a decision about people and release cadence that happens to be implemented in code. The most cited description defines micro-frontends as "an architectural style where independently deliverable frontend applications are composed into a greater whole" (Cam Jackson, on martinfowler.com, 19 June 2019). The word that matters is independently deliverable. If two pieces of a front end still have to be built, tested and released together, they are not independent, however many repositories they live in.
Three illustrations run through the lesson, each labelled where it appears:
- Minimal example: a container that mounts two independently written pieces through one small contract, with output from a real run.
- Realistic example: the grocery storefront deciding whether to split, in "In practice".
- Failure example: a "distributed front-end monolith", where the pieces are separate on paper and coupled in practice.
This lesson gives the working definitions and the decision. How the pieces are put together in the browser, on a server or at the edge is the subject of the next lesson, Micro-Frontend Composition: Build-Time, Runtime, Iframes and the Edge, and one specific tool, Module Federation, has its own lesson after that (Module Federation in React).
Three words: monolith, modular monolith, micro-frontends
A front-end monolith is one front-end application with one build and one deployable output. All teams contribute to the same codebase and ship through the same pipeline. That is not an insult: it is the simplest thing that works, and Web Architecture History shows that most apps start there.
A modular monolith is still one build and one deployment, but the code is organised in modules with explicit boundaries, so that teams own areas of the codebase and cannot reach into each other's internals. Enforcing those boundaries (folder rules, lint rules, package boundaries) is a separate topic in this course; here it matters as the cheaper alternative to a split.
Micro-frontends go further: the pieces are built and released independently and composed into one product when the user loads it. Cam Jackson's article describes the technique as taking the ideas of microservices and applying them to the front end, and says Thoughtworks first listed it on its Technology Radar in 2016. Michael Geers, whose site micro-frontends.org is the other widely cited reference, describes the same idea as treating a web app as "a composition of features" owned by independent teams, where each team is cross-functional and builds its feature end to end.
Figure 1. One product, several independently deliverable pieces
How to read this: the shopper sees one site. The container decides which piece renders where, and each piece has its own team and its own release pipeline. The arrows show the container asking for a piece, not how it is delivered: that choice is the subject of the next lesson.
What the idea promises
Jackson's article lists four benefits. Each is a claim about delivery, not about speed in the browser, so read them as hypotheses to test against your own organisation.
- Incremental upgrades. Teams can replace an old application piece by piece while still shipping features. If a main framework has a breaking change, each piece can be upgraded when it makes sense instead of all at once.
- Simple, decoupled codebases. Smaller codebases are easier to work with, and clear boundaries around business contexts make accidental coupling harder.
- Independent deployment. Each piece has its own continuous delivery pipeline, so a team can release without coordinating with other teams or the monolith's release cycle.
- Autonomous teams. Teams are organised around vertical slices of business functionality rather than horizontal technical concerns, and a common approach is one piece per page, owned end to end by one team.
Geers adds a set of working principles: be technology agnostic, isolate team code (avoid sharing a runtime or global state even when teams use the same framework), establish team prefixes for CSS, events, local storage and cookies where full isolation is not possible, favour native browser features over custom APIs, and build a resilient site.
The common thread is a trade: coordination cost goes down between teams, and integration cost goes up inside the product.
What it costs
The same article is direct about the downsides, and they are the parts a decision should weigh first.
- Payload size. Independently built bundles can duplicate common dependencies, for example one copy of a UI framework per piece. Sharing them reintroduces build-time coupling, which the article describes as a risk of a "one-off lockstep release event". It also notes that compiling per page acts like code splitting, so a page may still load faster overall, and recommends measuring real-world performance rather than assuming.
- Environment differences. A piece developed in isolation can behave differently once integrated with the others, particularly because of global styles. The article suggests integrating into production-like environments early and often.
- Operational and governance complexity. More repositories, pipelines, servers and domains mean more to manage. The article asks whether the organisation has the automation, release process and governance maturity to avoid "creating chaos".
Two more costs follow from the way the pieces talk to each other. The article recommends that pieces "communicate as little as possible", prefers custom events, callbacks and the address bar over shared state, and warns that shared data structures create heavy coupling. CSS is the other shared surface: it is global and cascading, so conflicting selectors from different teams can break a page, and the article's advice is that the technique matters less than making sure developers can write their styles independently and trust them to behave when composed.
Minimal example: a container and a contract
The smallest useful micro-frontend architecture is a contract and a container that knows only the contract. In Jackson's demo the contract is a global render function (and an unmount function) that each piece exposes, which the article calls the key contract between the container and the pieces. The code below is a simplified model of the same idea, not a browser integration: the "element" is a plain object, so it can run anywhere.
interface MicroFrontend {
mount(host: { name: string; html: string }, props: Record<string, string>): void;
unmount(host: { name: string; html: string }): void;
}
const catalogue: MicroFrontend = {
mount(host, props) {
host.html = `<h2>Catalogue</h2><p>Category: ${props.category}</p>`;
},
unmount(host) {
host.html = "";
},
};
const account: MicroFrontend = {
mount(host, props) {
host.html = `<h2>Account</h2><p>Signed in as ${props.user}</p>`;
},
unmount(host) {
host.html = "";
},
};
const routes: Record<string, MicroFrontend> = {
"/shop": catalogue,
"/account": account,
};
const slot = { name: "main-slot", html: "" };
let current: MicroFrontend | undefined;
function navigate(path: string, props: Record<string, string>): void {
const next = routes[path];
if (!next) {
console.log(`${path}: no micro-frontend registered`);
return;
}
current?.unmount(slot);
next.mount(slot, props);
current = next;
console.log(`${path}: ${slot.html}`);
}
navigate("/shop", { category: "Fruit", user: "" });
navigate("/account", { category: "", user: "sam" });
navigate("/orders", { category: "", user: "" });
This file was run with Node.js 26.12 (which runs TypeScript files directly by removing the types). It printed:
/shop: <h2>Catalogue</h2><p>Category: Fruit</p>
/account: <h2>Account</h2><p>Signed in as sam</p>
/orders: no micro-frontend registered
Three things to notice. The container never imports catalogue or account internals, only the contract. Switching routes unmounts the old piece before mounting the new one, so a piece cannot leave state in the slot. And an unknown route is handled by the container, not by any piece. A real implementation must also solve how the code of each piece reaches the browser, and that is exactly where the approaches of the next lesson differ.
Figure 2. One navigation through the container
How to read this: the container owns routing and the slot, and the pieces own what is rendered inside. A route is part of the contract, so a change to a path is a change that more than one team has to know about.
Where to cut: vertical slices, not layers
The first decision after "should we split" is where the seams go. Jackson's article is explicit that teams should be organised around vertical slices of business functionality, not around horizontal technical concerns, and that one piece per page is a common starting point.
A vertical slice contains everything needed to deliver one piece of user value: the screens, the state, the calls to the back end and the tests. A horizontal layer is a technical tier shared by many features: "the buttons", "the data-fetching layer", "the forms library".
| Cut | Example | What happens |
|---|---|---|
| Vertical, by business area | Catalogue, Checkout, Account | A feature change usually touches one piece and one team |
| Horizontal, by technical layer | "UI team", "API client team", "state team" | Every feature needs all the teams, so every release is coordinated |
| By component | Header as its own micro-frontend | Often a shared dependency in disguise; see below |
A cheap test for a seam: list the last ten changes your teams shipped and ask how many files each change touched across the proposed boundary. If most changes cross it, it is the wrong seam. The same reasoning drives service boundaries on the back end, which Decomposing Microservices: Data Ownership and Distributed Monoliths treats in depth.
Shared pieces such as the header, the design tokens and the authentication state are real and unavoidable. Treat them as small, stable, versioned shared dependencies with an owner, not as micro-frontends of their own, and keep the contract small. How shared code is deployed without lockstep releases is a later lesson in this module, so it is only named here.
Deciding: do you need this?
Micro-frontends solve an organisational scaling problem. Signs that you may have it:
- Several teams change the same codebase and regularly block each other on merges, release slots or test suites.
- A change in one area forces a full redeploy and regression test of the others.
- Teams want to upgrade frameworks or libraries at different times, and the single dependency tree prevents it.
- An old application is being replaced gradually and the new and old code must coexist.
Signs that you probably do not:
- One team, or a few teams that talk daily, own the whole front end.
- The pain is slow builds, a messy folder structure or tangled imports. A modular monolith, better tooling and build caching address those without a runtime composition layer.
- The product needs one tightly integrated experience with heavy shared state, where every screen depends on the same data.
- No one can own the platform work: pipelines, shared shell, integration testing, governance.
Figure 3. Should this front end be split?
other on build, test
or release?"} Q1 -->|"no"| M["Stay one app.
Revisit when it hurts"] Q1 -->|"yes"| Q2{"Are the business
areas and owners
clear?"} Q2 -->|"no"| MM["Draw the boundaries
inside one codebase first
(modular monolith)"] Q2 -->|"yes"| Q3{"Would independent
releases remove the
blocking?"} Q3 -->|"no"| T["Fix tooling:
build caching, test selection,
ownership rules"] Q3 -->|"yes"| Q4{"Can someone own the shell,
integration tests and
shared dependencies?"} Q4 -->|"no"| MM Q4 -->|"yes"| MF["Split along vertical slices,
one slice at a time"]
How to read this: each question is a gate, not a score. The answer "no" at the last gate sends you back to a modular monolith because a split without a platform owner moves the coordination problem into production.
Trade-offs at a glance
| Dimension | Front-end monolith (modular) | Micro-frontends |
|---|---|---|
| Deployment | One pipeline, one release | One pipeline per piece, plus a container |
| Team coordination | Needed for every release | Needed only at the contract |
| Framework upgrades | All at once | Per piece, when it suits the team |
| Initial and operating cost | Lower: one build, one place to look | Higher: more pipelines, servers, repositories and governance |
| Bundle size | One dependency tree, easy to share | Duplicated dependencies unless shared, which couples releases |
| Consistent look and behaviour | Enforced by one codebase | Needs a design system and conventions |
| Testing | End-to-end tests on one artifact | Contract tests plus integrated tests in a production-like environment |
| Failure blast radius | One bad release affects everything | A bad piece can be isolated, if the shell is built to tolerate it |
Failure example: the distributed front-end monolith
A distributed front-end monolith has the costs of a split and none of the independence. It appears when the seams are technical, or when the pieces share too much.
Picture the grocery storefront after a split into Catalogue, Checkout and Account, where all three read the signed-in user and the basket from one shared global object that the container fills. Checkout then needs a new field on that object. The change is small, but now Catalogue and Account must be updated to ignore or handle the field, and the container must write it. The four releases are ordered and tested together, and a rollback of one needs a rollback of all.
Figure 4. A shared object turns independent pieces into one release
How to read this: the red box is the coupling. Any change to its shape needs every reader updated, so the three "independent" pieces are released as one. Jackson's article advises against shared state for this reason and prefers custom events, callbacks and the address bar.
Other ways the same failure shows up: a shared component library that every piece must upgrade at the same time, a design system consumed as a build-time dependency (the article warns that build-time integration forces "a lockstep release process"), and routes that several pieces must agree on without a test that checks them. The remedy is usually to shrink the contract, make it explicit and test it, as the article suggests with consumer-driven contract tests.
The other side of this topic
On the back end the same decision is about services and data ownership: a boundary pays only if a team can change, deploy and fail without the others, and the lesson Decomposing Microservices: Data Ownership and Distributed Monoliths gives the tests for coupling in deployment, data and runtime. The front end has the same three couplings in different clothes: a shared build, a shared global state or shared styles, and a runtime that fails together. Everything you would check for a service boundary has a front-end counterpart.
In practice: a grocery storefront decides whether to split
This is a realistic scenario, not a case history. It is a composite built from common patterns, describes no specific company, and every number in it is an illustrative assumption.
The situation
The storefront's front end is one React application in one repository with one pipeline. Four teams (illustrative) now contribute: Catalogue, Checkout, Account and a platform team that owns the shell. Over the last year, release slots have become a negotiation. Checkout wants to ship a payments change every few days, while Catalogue ships a redesign in stages and keeps a large test suite red for hours. Product asks whether to split the front end into micro-frontends "like the big sites do".
Starting architecture
Figure 5. One build, one release train
How to read this: three team streams merge into one repository and one pipeline, so any team's change waits for the same suite and the same release.
Options considered
| Option | What it fixes | What it costs | Risk |
|---|---|---|---|
| Do nothing, add a release calendar | Predictability | Teams keep waiting | Frustration grows |
| Modular monolith: enforce module boundaries, per-module test selection, build caching | Merge conflicts, slow suites, unclear ownership | Rules and tooling work | Still one release train |
| Split everything into micro-frontends | Release independence for all areas | A shell, a contract, pipelines, integration testing, governance | Distributed monolith if seams or contract are wrong |
| Split Checkout only, keep the rest as one app | Release independence where the pressure is | One container, one contract, one more pipeline | Two ways of working; the contract must stay small |
The decision
| Field | Decision record |
|---|---|
| Context | Checkout needs release independence. Catalogue and Account do not block anyone. The platform team can own a shell. |
| Decision | Do the modular-monolith work first (boundaries, test selection, build caching). Then extract Checkout as the first micro-frontend behind a route, keep a written contract, and leave the rest in the main app. |
| Consequences, good | Checkout releases on its own cadence. The rest gains from the boundary work even if nothing else is ever split. Risk stays small. |
| Consequences, bad | A shell and contract to maintain. Duplicate framework weight on the Checkout route unless a shared dependency is agreed. Integration tests needed. |
| Revisit when | A second area has the same release pressure, or the contract grows beyond a short list of events and props. |
Rollout and verification
- Draw and enforce boundaries inside the monolith and check that most recent changes stay inside one module (the ten-changes test above).
- Write the contract between shell and Checkout: the route, the props it receives, the events it may emit. Keep it to a short list and cover it with contract tests.
- Extract Checkout behind the route while the old Checkout code remains deployable. Switch traffic gradually, one route at a time, which is the incremental-upgrade pattern the article describes.
- Test integrated, not only standalone. Run the end-to-end flow in a production-like environment, because developing a piece alone can hide problems that global styles cause once it is composed.
- Measure the Checkout route's loading before and after, as the article recommends, instead of assuming the split is faster or slower.
The 2 a.m. failure
A Checkout release goes out in the evening. Late that night, support sees that buttons on the Account page look wrong and some are unreadable. Nothing in Account changed.
The cause: a Checkout developer added a global rule for all button elements to match a new payments style. In standalone development the piece looked right. Once loaded into the shell, the rule applied to the whole page, which is the global cascade problem the article describes.
First three checks: (1) compare the page's loaded stylesheets before and after the release to find the new rule; (2) check whether the rule is scoped by a team prefix or a component boundary; (3) check whether the integrated test environment had visual coverage of other areas' pages. Mitigation: roll Checkout back to its previous release, which is possible because it is independently deployed. Follow-up: require a team prefix or a scoped styling approach, add a visual check of neighbouring pages to the integrated test, and add a review rule that no piece may style elements it does not own.
Architect's notes
- Decide on teams and release pain, not code size. A large codebase with one team is a tooling problem. A small codebase with five teams fighting over releases may be an architecture problem.
- Boundaries first, runtime composition second. Drawing and enforcing module boundaries is cheap and useful either way, and it tells you whether the seams hold.
- Keep the contract small and tested. Routes, props, events and the shared shell are public API. Version them and test them.
- Budget for a platform owner. The shell, shared dependencies, integration environment and governance need a named team.
- Measure the payload. Count the duplicated dependencies and the extra requests on the heaviest routes before and after, and decide on real data.
- Split one slice, learn, then decide. The first extraction teaches more than any plan.
Common mistakes
- Splitting by technical layer. A "header team" and a "forms team" produce coordinated releases. Split by business slice.
- Sharing global state between pieces. It looks convenient and recreates the monolith's coupling at runtime.
- Treating the split as a performance feature. It is a delivery and ownership feature, and it can make payloads larger.
- Testing each piece only on its own. Global styles and shared dependencies fail only when the pieces meet.
- Starting with too many pieces. Each piece needs a pipeline, an owner and a contract.
- Ignoring governance. Without conventions, the pieces drift in design, accessibility and tooling.
- Splitting when the real problem is a slow build. Fix the build.
Key takeaways
- Micro-frontends are an architectural style where independently deliverable front-end applications are composed into one product.
- The promised benefits are incremental upgrades, simpler decoupled codebases, independent deployment and autonomous teams.
- The documented costs are payload size, environment differences, and operational and governance complexity.
- The decision is about teams and release coupling. A modular monolith is the cheaper first step and often the right answer.
- Cut along vertical business slices, not technical layers.
- Keep pieces communicating as little as possible, preferring events, props and the address bar to shared state.
- A distributed front-end monolith has the costs of a split without the independence: look for shared state, build-time shared libraries and untested routes.
- Test integrated in a production-like environment, and measure real performance.
Practice exercises
Exercise 1 — Starter (30 min): For a front end you know, list the last ten changes and which areas each touched. Draw the seams a split would use and count how many changes cross them. Write one sentence on whether the numbers support a split.
Exercise 2 — Intermediate (2 hours): Take the minimal container above and add a third piece with a different props shape. Add a function that checks each piece has mount and unmount before registering it, and write down what the container should do when a piece throws in mount.
Exercise 3 — Advanced (half day): Write a one-page decision record for splitting a front end of your choice, using the table format in this lesson. Include the contract (routes, props, events), the shared dependencies and their owner, the first slice to extract, and the signal that would make you stop.
Related reading
In this course:
- From Multi-Page Sites to SPAs and Back: How Web App Architecture Evolved, where the monolithic SPA came from.
- Code Splitting, Lazy Loading and Tree Shaking, the per-route loading that micro-frontends build on.
- Micro-Frontend Composition: Build-Time, Runtime, Iframes and the Edge, the next lesson: how the pieces are assembled.
- Frontend Trade-Off Analysis, a way to write down a decision like this one.
Elsewhere on the site:
- Decomposing Microservices: Data Ownership and Distributed Monoliths, the back-end view of the same decision.
Official documentation
- Micro Frontends, by Cam Jackson (martinfowler.com), the definition, benefits, integration approaches and downsides used in this lesson.
- Micro Frontends, by Michael Geers (micro-frontends.org), the core ideas and the integration spectrum.
- Module Federation: Glossary of terms, its definition of a micro-frontend.
- MDN: Web components, the browser features that can serve as a contract.