Every Terraform project eventually faces this question: do we keep everything in one repository, or split it up? There's no universal answer, but there are patterns that work better depending on your situation.

The Two Approaches

A monorepo keeps all your Terraform code in a single repository. Modules, environments, pipelines—everything lives together. Changes are atomic, and you can see the entire infrastructure at a glance.

A polyrepo splits infrastructure across multiple repositories. Each team or component gets its own repo. Changes are isolated, and teams can move independently.

How Modules Change the Equation

The real complexity comes from modules. In a monorepo, you reference modules with relative paths. Change a module, and every consumer sees it immediately—for better or worse.

# Monorepo: relative path, always current
module "network" {
  source = "../../modules/networking"
}

In a polyrepo, modules live in their own repository and consumers pin to specific versions. This gives you control over when changes propagate.

# Polyrepo: versioned reference
module "network" {
  source = "git::https://dev.azure.com/org/infra/_git/terraform-modules//networking?ref=v2.1.0"
}

When Monorepo Works

Monorepo shines when your infrastructure is tightly coupled. Hub-and-spoke networks are the classic example—the hub and spokes need to know about each other. Keeping them together makes sense.

It also works well for smaller teams (3-10 engineers) where everyone needs visibility into everything, and the overhead of versioning modules outweighs the benefits.

When Polyrepo Works

Polyrepo becomes valuable when teams need autonomy. Different deployment cadences, different security requirements, different approval chains. The network team deploys monthly; the app team deploys daily. Forcing them into one repo creates friction.

Here's the counterargument to "hub-spoke needs monorepo": if your spoke module is designed smartly, it can handle peering automatically. Bake the hub VNet IDs into the module, and spoke teams don't need access to hub code at all.

# Spoke module with built-in hub knowledge
locals {
  hub_vnets = {
    production  = "/subscriptions/.../hub-prod-vnet"
    development = "/subscriptions/.../hub-dev-vnet"
  }
}

resource "azurerm_virtual_network_peering" "to_hub" {
  remote_virtual_network_id = local.hub_vnets[var.environment]
}

The Translation Module Pattern

One pattern that enables polyrepo at scale: the shared variables module. It's not a module that creates resources—it's a module that translates organizational conventions.

Pass in "dev" and get back "d" for naming. Pass in "eastus2" and get back "use2". Everyone uses the same abbreviations, and the platform team controls them centrally.

module "naming" {
  source      = "git::https://dev.azure.com/org/platform/_git/terraform-modules//shared-variables?ref=v1.5.0"
  environment = var.environment  # "dev" → "d"
  location    = var.location     # "eastus2" → "use2"
}

resource "azurerm_storage_account" "main" {
  name = "st${var.project}${module.naming.region_char}${module.naming.env_char}"
}

The Decision

Start with these questions:

  • How many teams touch this infrastructure?
  • Do they deploy on different schedules?
  • Is there natural coupling between components?
  • Do you have the platform maturity to maintain versioned modules?

Small team, coupled infrastructure, rapid iteration → monorepo. Multiple teams, independent deployments, strict boundaries → polyrepo with a solid module strategy.

Most organizations end up hybrid: a shared modules repo consumed by team-specific infrastructure repos. The modules repo is the glue—providing consistency without coupling.