The change itself was small. A shared component needed one more input, and the places that used it needed to pass it.
The change took an afternoon. Landing it took three weeks.
The shape of it: a platform of seven-plus micro-frontends, each in its own repository, each owned by a different team, composed at runtime. The design system lived in its own repository too, published as a versioned package. So the sequence was fixed. Change the component. Review it. Release a new version of the package. Then open a pull request in every consuming repository to bump the version and pass the new input. Each of those went into a different team's queue, behind that team's own work, on that team's own release train.
Nine pull requests for one idea. And the whole thing only stayed correct if none of them stalled long enough to be forgotten.
Two decisions wearing one name
Micro-frontends promise independent deployability. That promise is worth real money: a team ships when its work is ready, without waiting for a platform-wide release, and a bad deploy takes out one section instead of the product.
Here is what I had conflated, and what I think the industry conflates constantly. That promise says nothing about source control. Independent deployment is a property of how applications are built and served. Independent repositories are a second choice, made at the same moment, usually for a reason that sounds like "each team should own their code" rather than anything about deployment.
Splitting the repositories buys ownership clarity. It bills you for three things.
Shared code has to be published to travel. A component, a model, a validation rule — anything used in more than one place becomes a package with a version number and a release process. That is a lot of ceremony around a function.
Cross-cutting changes cannot be atomic. There is no commit that changes the contract and its consumers together, so there is always a window where the repositories disagree. You manage that window with backwards compatibility, coordination, and hope.
And integration is only proven after assembly. Every repository's pipeline passes on its own. The interesting failures live between them, and the first honest test is the running page.
None of that is a flaw in micro-frontends. It is the bill for a second split nobody costed.
The wall was doing two jobs
The strongest argument I hear for separate repositories is that they enforce boundaries. A team cannot casually reach into another team's internals if the code is not there to reach into.
That is true, and it is worth taking seriously. But look at what the wall is made of. It stops the reach by making the code absent. That also stops every legitimate cross-cutting change, atomic upgrade, and whole-graph refactor. It is a boundary with no vocabulary. It cannot say "this dependency is fine and that one is not." It can only say "nothing."
In a single workspace, you get to say the specific thing. Nx tags each project, then a lint rule decides which tags may depend on which:
// apps/mfe-reports/project.json — the project declares what it is
{ "name": "mfe-reports", "tags": ["type:app", "scope:reports"] }
// eslint.config.mjs — and the rule says what that lets it reach
'@nx/enforce-module-boundaries': ['error', {
allow: [],
depConstraints: [
{ sourceTag: 'scope:reports', onlyDependOnLibsWithTags: ['scope:reports', 'scope:shared'] },
{ sourceTag: 'scope:settings', onlyDependOnLibsWithTags: ['scope:settings', 'scope:shared'] },
{ sourceTag: 'scope:shared', onlyDependOnLibsWithTags: ['scope:shared'] },
],
}]
An import that crosses a line it should not fails in the editor and in CI, with the rule that rejected it named in the message:
error A project tagged with "scope:reports" can only depend on libs tagged
with "scope:reports", "scope:shared" @nx/enforce-module-boundaries
The boundary is still enforced. It is just written down now, reviewable and specific. And the same workspace that refuses the illegal import allows the legal cross-cutting one in a single commit. The design-system change and its nine consumers land together, reviewed as one unit, tested against every consumer before the merge instead of after the deploy. The deploys stay independent, so a version window still exists at runtime — but it is one you chose, between two artifacts you built together, not one that opens because nine queues moved at different speeds.
The runtime federation seam is untouched by any of this. The applications still build separately, deploy separately, and load each other at runtime by manifest. That is the part people expect to lose and do not: sharing a repository is not sharing a release. You can see the two coexisting on the federated shell demo — one workspace, one design-system package, and a host that still resolves both remotes at runtime with no compile-time reference to either.
Where the polyrepo is the right answer
This is a real trade, not a solved question, and I have seen the separate-repository layout be clearly correct.
When the boundary is organizational rather than technical, the repository should follow the organization. Think of different legal entities, an outside vendor building one section, a domain under its own compliance regime. In all of those, "who can read this code" is a governance question, and it was answered before your architecture existed. A lint rule is the wrong instrument for it.
Separate repositories also give unambiguous ownership, and that has value beyond enforcement. Fewer people in the commit history, a review queue that is entirely yours, no possibility of a well-meaning stranger refactoring your module on a Friday. Teams that have been burned by a shared codebase are not being irrational; they are pricing in something real.
And the monorepo's costs are not small. The tooling has to be good enough that the graph is actually navigable, CI has to scale to a workspace where any change might affect anything, and someone has to own that tooling as real work rather than a side project. The hardest part is not technical at all — it is the culture shift of agreeing that other people may open a pull request against your code, and that this is a feature.
For teams inside one organization shipping one product surface, I would take that cost. The coordination tax I described at the top is paid on every cross-cutting change forever, and it compounds as the platform grows. The monorepo tax is paid mostly once, in tooling, up front. Where the boundary is genuinely organizational, I would keep the repositories and accept the ceremony, because there the wall is protecting something a lint rule cannot.
Independent Deployment Is the Goal, Not the Repository Count
The reason micro-frontends exist is that a team should be able to ship without asking permission. Everything else is implementation. Separate repositories are one way to get there, and the most expensive one. They buy the deployment independence you wanted by also buying a source-control split you did not need. Then they bill you for the second one on every change that crosses a boundary. Before splitting a repository, ask which of the two things you are actually buying. If the answer is only the first, there is a cheaper way to pay for it.


