Polyrepo versioning with monorepo speed
The monorepo vs polyrepo argument runs through teams and the Terraform eco-system rearing its head continuously, and it never resolves because both sides are right about the other side's problem. I think it is a false choice, and the reason it feels like a real one is that the thing that dissolves it does not live in either repo layout. It lives in the registry.
Let me start with the two failure stories, because everyone reading this has lived at least one.
The two failures
Polyrepo: every module gets its own repo, its own version, its own release notes, its own owners. Clean. Then you make a breaking change to your vpc module, and updating the fifteen things that consume it means fifteen pull requests, each one a hand-edited version bump, each one discovered by memory or by grep across repos you may not even have checked out. The versioning is immaculate and the change is a week of tedium.
Monorepo: everything in one repo, so the same breaking change is one PR that updates the module and every consumer together, atomically. Fast. But now individual modules do not really have versions, or owners, or release notes, because the repo is the unit and the repo has one history. You traded per-module discipline for speed.
Each layout is a good answer to the problem the other one has. That is the tell that the layout is not the real variable.
What teams actually want
Strip it back. What people want from polyrepo is the semantics: each module individually versioned, separately change-tracked, with its own release notes and owners. What they want from monorepo is the speed: one change, one PR, dependents updated mechanically instead of by hand.
Those two are only in tension if nothing knows how the modules relate. The missing piece is a component that holds the dependency graph, that knows your app module consumes vpc, s3, and ec2 at specific versions. Once something knows the graph, version resolution, change fan-out, and impact analysis all become mechanical, and mechanical is exactly the work that makes polyrepo slow and the discipline that monorepo loses.
The JavaScript world solved this a decade ago
If this feels familiar, it should, because the JavaScript ecosystem hit the identical wall and climbed out of it years ahead. The parallels are close enough to be instructive, so use them deliberately.
npm has semver ranges. ^1.2.0 and a lockfile mean you resolve versions instead of hand-editing exact pins in fifteen places, which is precisely the exact-pin tedium Terraform teams live in. Changesets gives JS monorepos independent per-package versioning and changelogs inside one fast workflow, which is direct proof that individual versioning and monorepo speed are not mutually exclusive; it is a solved problem in another language. Nx and Turborepo do affected-graph builds, only rebuilding and retesting what a change actually touches, which is the same idea as generating work from a module dependency graph rather than rebuilding the world. And Renovate and Dependabot automate the dependency-bump PRs that Terraform teams currently raise by hand.
The one-line version: Terraform module management today is npm before lockfiles, workspaces, and Renovate. The capabilities exist as ideas, they just have not been built into the layer that can see the whole graph. And in Terraform, that layer is the registry, because the registry is the only thing that sees every module and every consumer at once. A repo only sees itself.
The worked example
Note; you can find a real world example that populates the acme-demo org in Terramantle in the Terramantle Module Sample repository. Take an application module that depends on network and storage, with the modules in separate, individually-versioned repos:
acme-app/
main.tf # consumes vpc, s3, ec2 from the registry
versions.tf
# separate repos, each with its own version
terraform-aws-vpc v2.4.1
terraform-aws-s3 v1.9.0
terraform-aws-ec2 v3.1.2
The main.tf is unremarkable, just module blocks with version constraints:
module "vpc" {
source = "registry.terramantle.dev/acme/vpc/aws"
version = "~> 2.4"
}
Now walk the breaking change both ways.
Before, without a graph: you bump vpc to v3.0.0 in its repo, then you go find every consumer. You grep, you check your memory, you hope. For each one you raise a PR and hand-edit the constraint. You discover the fourteenth consumer when it breaks in production because it was in a repo you forgot existed. The versioning was clean and the change was a manual archaeology project.
After, with the registry holding the consumer graph: before you even merge the bump, you can see every workspace pinned to vpc and how each constrains it, so the blast radius is a number you know rather than a surprise you get. The version resolution against ~> 2.4 is mechanical. What still needs a human is deciding to make the change and reviewing it; what stops needing a human is finding out who it hits.
I want to be precise about what the graph does today, because this is where thought-leadership posts usually overclaim. The registry gives you the visibility: the graph, the consumers, the constraints, the blast radius before you ship. It does not currently open the fourteen pull requests for you. The automatic fan-out, the Renovate-for-Terraform that raises the consumer PRs, is the natural next thing the graph makes possible, and framing it honestly: the graph is the missing piece that lets that tooling exist, not a button that exists today. The visibility is real and shipping. The auto-fan-out is where the visibility leads.
That distinction matters, because the visibility alone already dissolves the false choice. The reason polyrepo fan-out is slow is that you cannot see the consumers. Once you can, the manual PRs are tedious but bounded, not a fishing expedition. You get polyrepo's clean per-module versioning and most of monorepo's speed, from a registry that knows the graph, without picking a repo layout at all.
See it on something real
Rather than screenshots, the Terramantle demo populates an demo org (ACME-DEMO) with modules that actually depend on each other, so you can click through a living dependency graph instead of looking at a picture of one. You can join the demo org from inside Terramantle and walk the graph yourself.
The modules are publicly available and Github Actions publishes them in continuously to see real graphs.
What still needs discipline
The graph amplifies process, it does not replace it. Semver hygiene still has to be real, because a graph that resolves ~> 2.4 is only as good as your honesty about what counts as a breaking change. Release notes still have to be written, because "here are your fourteen consumers" is less useful if none of them can tell what changed. Ownership still has to be assigned. Tooling makes good process fast and bad process fast; it does not make bad process good.
But the layout fight is a distraction. Pick whichever repo structure your team likes, and put a graph-aware registry underneath it. That is what actually gives you both.
The blast-radius question this answers is the visibility gap from the versioning post, and the reason the registry can hold this graph without touching your credentials is the same reason it does not run your Terraform.
Last reviewed
Terramantle is a private Terraform and OpenTofu registry, in beta. See pricing or read the FAQ.