Skip to content
Infrastructure as Code iac patterns 4 min read

Monorepo vs Multi-Repo

One of the earliest and most consequential decisions in an Infrastructure as Code practice is where the code lives. Do you keep every Terraform configuration, module, and environment in a single repository, or do you split infrastructure across many focused repositories owned by different teams? The choice shapes your blast radius, your pipelines, your dependency management, and how easily engineers can reason about changes. This page compares both models, weighs the trade-offs, and gives you a framework for deciding.

What each model means

A monorepo stores all infrastructure code in one version-controlled repository. Shared modules, environment configurations, and root stacks live side by side. A change to a module and the stacks that consume it can land in a single atomic commit.

A multi-repo (sometimes called polyrepo) splits infrastructure into many repositories, typically one per team, service, or domain. Each repo owns its own pipeline, its own state, and its own release cadence. Shared modules are usually published as versioned artifacts and pulled in by reference.

Both work with Terraform 1.5+ and OpenTofu identically — neither tool cares how your filesystem is organized across repositories. The difference is entirely about workflow, ownership, and coordination.

A typical monorepo layout

In a monorepo, you co-locate modules with the stacks that use them and reference them by relative path.

# environments/prod/network/main.tf
module "vpc" {
  source = "../../../modules/vpc"

  cidr_block         = "10.20.0.0/16"
  availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
  enable_nat_gateway = true
  environment        = "prod"
}

output "vpc_id" {
  value = module.vpc.vpc_id
}

Because the module is referenced by path, editing modules/vpc and the prod stack in one commit guarantees they stay consistent. Running a plan from the stack directory picks up the local change immediately:

cd environments/prod/network
terraform init
terraform plan

Output:

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # module.vpc.aws_vpc.this will be created
  + resource "aws_vpc" "this" {
      + cidr_block = "10.20.0.0/16"
      + id         = (known after apply)
    }

Plan: 9 to add, 0 to change, 0 to destroy.

A typical multi-repo layout

In a multi-repo setup the vpc module lives in its own repository and is published with a version tag. Consuming repos pin to a specific version, so a module change does not affect anyone until they deliberately upgrade.

# In the "platform-network" repo
module "vpc" {
  source = "git::https://github.com/acme/terraform-aws-vpc.git?ref=v2.4.1"

  cidr_block         = "10.20.0.0/16"
  availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
  enable_nat_gateway = true
  environment        = "prod"
}

A private registry (Terraform Cloud, an internal registry, or an S3-backed source) achieves the same versioned isolation:

module "vpc" {
  source  = "app.terraform.io/acme/vpc/aws"
  version = "~> 2.4"
  # ... inputs
}

Always pin module versions in a multi-repo. A floating ref=main reintroduces the very coupling you split the repos to avoid — and makes plans non-reproducible across runs.

Trade-offs at a glance

DimensionMonorepoMulti-repo
Atomic cross-cutting changesEasy — one commit, one PRHard — coordinate multiple PRs
Module versioningOptional (path refs)Required (tags/registry)
Blast radius of a bad changeLarger by defaultNaturally contained per repo
Access control / ownershipCoarse (one repo’s permissions)Fine-grained (per team)
CI/CD pipelineOne pipeline, needs path filteringIndependent per repo
DiscoverabilityHigh — everything is searchableLower — code is scattered
OnboardingClone one repo, see the whole pictureHunt across many repos
Scaling to many teamsMerge contention, slow CIScales cleanly

Choosing between them

The decision usually comes down to organizational shape, not technology. Use this framework:

  • Team count and autonomy. A handful of engineers sharing ownership favors a monorepo. Many independent teams that ship on their own schedules favor multi-repo.
  • Change frequency across boundaries. If most changes touch several stacks at once, the monorepo’s atomic commits save real pain. If changes are mostly local to one service, isolation wins.
  • Compliance and access. Strict per-team or per-environment access control is far easier with separate repos and separate pipelines.
  • State management. Either model should still split state into many small backends; a monorepo is not a single state file. See Managing large state.

A common middle ground is a modular monorepo with path-scoped CI: keep one repo, but trigger pipelines only on changed directories so teams are not blocked by unrelated changes.

# Plan only the stacks affected by this push (path-filtered CI)
git diff --name-only HEAD~1 HEAD \
  | grep '^environments/' \
  | cut -d/ -f1-3 | sort -u \
  | while read dir; do terraform -chdir="$dir" plan; done

Many teams start with a monorepo for velocity and split out repos only when CI contention or ownership boundaries become painful. Premature splitting adds versioning overhead before you need it.

Best practices

  • Split Terraform state into small, independent backends regardless of repo strategy — repo layout and state layout are separate decisions.
  • In multi-repo setups, always pin module sources to immutable version tags or registry constraints; never track a branch.
  • Use path-filtered CI in a monorepo so a change in one environment does not run plans for the entire organization.
  • Apply consistent naming conventions across all repos so resources remain discoverable wherever the code lives.
  • Publish shared modules with semantic versioning and a changelog so consumers can upgrade deliberately.
  • Document the dependency graph between stacks — implicit ordering is the most common failure mode in both models.
  • Revisit the decision periodically; the right structure for five engineers is rarely the right structure for fifty.
Last updated June 14, 2026
Was this helpful?