Skip to content

    Frontend System Design

    Monolith vs Micro-Frontends: When to Split a Front End

    Decide whether a front end should stay one deployable app or be split into micro-frontends, using team ownership, release coupling and the costs of a split.

    By Published 18 min read

    Share

    Flagship lessonintermediateFrontend System Design

    Before this: Before Micro-Frontends: the Modular Monolith Front End (coming soon), JavaScript Modules and TypeScript at Scale (coming soon)

    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

    A container composes independently deployed micro-frontends into one pageShopper to Container app (request); Container app to Catalogue (loads on /shop); Container app to Checkout (loads on /checkout); Container app to Account (loads on /account)INDEPENDENT PIECESShopper: one URL, one pageShopperone URL, one pageContainer app: routing, shared shellContainer approuting, sharedshellCatalogue: team A, own pipelineCatalogueteam A, ownpipelineCheckout: team B, own pipelineCheckoutteam B, ownpipelineAccount: team C, own pipelineAccountteam C, ownpipelinerequestloads on /shoploads on /checkoutloads on /account

    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.

    1. 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.
    2. Simple, decoupled codebases. Smaller codebases are easier to work with, and clear boundaries around business contexts make accidental coupling harder.
    3. 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.
    4. 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

    sequenceDiagram accTitle: Navigation in a container with micro-frontends accDescr: The shopper clicks a link, the container resolves the route, unmounts the current piece, loads and mounts the next piece, and the piece renders into the slot. participant U as Shopper participant C as Container participant A as Current piece participant B as Next piece U->>C: clicks "Account" C->>C: resolve route to a piece C->>A: unmount(slot) C->>B: load code if not loaded C->>B: mount(slot, props) B-->>U: renders into the slot

    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?

    flowchart TD accTitle: Deciding whether to split a front end accDescr: A decision flow. If teams are not blocked by each other, stay a monolith. If boundaries are not clear, build a modular monolith first. If the pieces still need to ship independently and a platform owner exists, split along vertical slices; otherwise improve the monolith. S["Front end with several teams"] --> Q1{"Do teams block each
    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

    Pieces coupled through one shared global object must release togetherCatalogue to Shared global state (reads); Checkout to Shared global state (writes); Account to Shared global state (reads); Shared global state to Release train (shape change forces)Catalogue: reads shared basketCataloguereads sharedbasketCheckout: writes shared basketCheckoutwrites sharedbasketAccount: reads shared userAccountreads shared userShared global state: one shape for allShared globalstateone shape for allRelease train: ordered, togetherRelease trainordered, togetherreadswritesreadsshape change forces

    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

    NOTE

    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

    Starting architecture: four teams, one repository, one pipeline, one deployableCatalogue team to One repository; Checkout team to One repository; Account team to One repository; One repository to One release pipeline; One release pipeline to One deployed front end (release)Catalogue team: src/catalogueCatalogue teamsrc/catalogueCheckout team: src/checkoutCheckout teamsrc/checkoutAccount team: src/accountAccount teamsrc/accountOne repository: one build, one test suiteOne repositoryone build, onetest suiteOne release pipeline: all or nothingOne releasepipelineall or nothingOne deployed front end: served from a CDNOne deployedfront endserved from a CDNrelease

    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

    1. Draw and enforce boundaries inside the monolith and check that most recent changes stay inside one module (the ten-changes test above).
    2. 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.
    3. 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.
    4. 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.
    5. 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.

    In this course:

    Elsewhere on the site:

    Official documentation

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments