Monorepo vs Multirepo #

One of the first decisions to make when organizing Terraform configuration at team scale is: put everything in one repository, or split it into several? There’s no universal answer that fits every context. A monorepo makes visibility and cross-configuration refactoring easier; multirepo provides clearer isolation and ownership. The choice depends on team size, infrastructure architecture, and how you want to manage dependencies between components.

flowchart LR
    subgraph Mono["Monorepo"]
        M1["networking/"]
        M2["compute/"]
        M3["database/"]
        M4["shared modules/"]
    end
    subgraph Multi["Multirepo"]
        N1["infra-network\n(repo)"]
        N2["infra-compute\n(repo)"]
        N3["infra-db\n(repo)"]
    end

    style M1 fill:#3b82f6,stroke:#1e40af,color:#fff
    style M2 fill:#3b82f6,stroke:#1e40af,color:#fff
    style M3 fill:#3b82f6,stroke:#1e40af,color:#fff
    style M4 fill:#8b5cf6,stroke:#6d28d9,color:#fff
    style N1 fill:#10b981,stroke:#059669,color:#fff
    style N2 fill:#10b981,stroke:#059669,color:#fff
    style N3 fill:#10b981,stroke:#059669,color:#fff

Monorepo: Everything in One Place #

TYPICAL MONOREPO STRUCTURE:

infra-repo/
  ├── modules/                  ← Shared modules
  │   ├── networking/
  │   ├── compute/
  │   └── database/
  │
  ├── environments/
  │   ├── dev/
  │   │   ├── networking/
  │   │   ├── compute/
  │   │   └── database/
  │   ├── staging/
  │   │   └── ...
  │   └── production/
  │       └── ...
  │
  ├── .github/workflows/        ← One CI/CD pipeline
  ├── .gitignore
  └── README.md

All infrastructure lives in one repository, managed together.
MONOREPO ADVANTAGES:

  ✓ Full visibility — all changes are visible in one place
  ✓ Easier refactoring — change a module and update all its callers at once
  ✓ Dependencies between configurations are easier to track
  ✓ One CI/CD pipeline for everything
  ✓ Easier for small teams managing all the infrastructure
  ✓ No versioning complexity between modules and their callers

MONOREPO DISADVANTAGES:

  ✗ The repository can get big and slow over time
  ✗ One commit can affect CI/CD for all components
  ✗ Access control is harder (hard to restrict specific teams to specific folders)
  ✗ A careless module change can break all its callers
  ✗ Tighter coupling between teams

Multirepo: One Repository per Component #

TYPICAL MULTIREPO STRUCTURE:

  infra-networking-repo/
    ├── modules/vpc/
    ├── environments/dev/
    ├── environments/staging/
    └── environments/production/

  infra-compute-repo/
    ├── modules/ec2/
    ├── modules/asg/
    └── environments/...

  infra-database-repo/
    ├── modules/rds/
    └── environments/...

  infra-modules-repo/           ← Shared modules in a separate repo
    ├── networking/
    ├── compute/
    └── database/
    # Versions: v1.0.0, v1.1.0, v2.0.0

  Each team has its own repository with separate access and pipelines.
MULTIREPO ADVANTAGES:

  ✓ Clear isolation — changes in one repo don't affect the others
  ✓ Per-repository access control — the database team can't modify networking
  ✓ More specific and faster CI/CD pipelines
  ✓ Modules can have their own versioning (v1.0, v1.1, v2.0)
  ✓ Teams can move more independently

MULTIREPO DISADVANTAGES:

  ✗ Reduced visibility — hard to see the big infrastructure picture
  ✗ Complex dependency management — must manage module versions across repos
  ✗ Cross-repo refactoring is harder and needs coordination
  ✗ Pattern/convention duplication if there are no shared guidelines
  ✗ More management overhead (many repos, many pipelines)

Choosing Based on Context #

CHOOSE A MONOREPO IF:

  Small team (< 5 engineers):
    Multirepo overhead isn't worth it, everyone already knows all the
    infrastructure, a monorepo provides the needed visibility

  Tightly coupled infrastructure:
    Networking, compute, and database often change together
    Cross-component refactoring happens frequently

  Just starting with Terraform:
    Start simple, split into multirepos later if needed
    "Premature separation" is more dangerous than a monorepo that grows big

  One team managing all the infrastructure:
    No need for isolation between teams

CHOOSE MULTIREPO IF:

  Large team with clear ownership:
    Team A only touches networking, Team B only touches database
    Access isolation matters for governance

  Modules used by many teams:
    Modules need their own versions so they can be upgraded gradually
    A breaking module change doesn't directly affect all callers

  Compliance requirements:
    Audits must be separate per domain (network audit, data audit)
    Production database config access must not be in the same repo
    as compute

  Mature, stable infrastructure:
    Components rarely undergo cross-domain refactoring
    Separation is already natural according to team ownership

Hybrid: Monorepo per Domain #

An approach often used by medium-to-large teams is a monorepo per domain — one repo for networking, one for platform, one for applications, and a dedicated repo for shared modules.

HYBRID STRUCTURE:

  infra-foundation-repo/        ← Platform / Infrastructure team
    ├── networking/
    ├── security/
    └── shared-services/

  infra-platform-repo/          ← Platform Engineering team
    ├── kubernetes/
    ├── observability/
    └── cicd-infrastructure/

  infra-app-repo/               ← Application team
    ├── environments/dev/
    ├── environments/staging/
    └── environments/production/

  terraform-modules-repo/       ← Platform team — used by everyone
    ├── vpc/
    ├── eks/
    ├── rds/
    └── CHANGELOG.md

This provides enough isolation without excessive fragmentation.

Practices That Apply to Both Approaches #

There are several practices that matter regardless of monorepo or multirepo.

# 1. A single source of truth for the Terraform version
# Use .terraform-version or required_version in all configurations
terraform {
  required_version = ">= 1.6, < 2.0"
}

# 2. Always commit the lock file
# .terraform.lock.hcl must be in .gitignore EXCEPTION
# (exclude everything but include the lock file)
# .gitignore:
# .terraform/
# !.terraform.lock.hcl  ← this one is included

# 3. CODEOWNERS for review control
# .github/CODEOWNERS:
# /environments/production/  @infrastructure-team @senior-engineers
# /modules/                  @platform-team

# 4. Consistent directory conventions
# Regardless of monorepo/multirepo, the internal structure must be uniform
# environments/
#   dev/        staging/        production/
# (not: dev/  stg/  prod/ — inconsistency is confusing)

flowchart TD
    A["Small team\nfew services"] --> B["Monorepo ✅"]
    C["Large team\nmany services"] --> D["Multirepo ✅"]
    E["Microservices\nstrong ownership"] --> D
    F["Shared infra\nrefactoring needed"] --> B

    style A fill:#3b82f6,stroke:#1e40af,color:#fff
    style B fill:#10b981,stroke:#059669,color:#fff
    style C fill:#f59e0b,stroke:#d97706,color:#fff
    style D fill:#10b981,stroke:#059669,color:#fff
    style E fill:#8b5cf6,stroke:#6d28d9,color:#fff
    style F fill:#3b82f6,stroke:#1e40af,color:#fff

Hybrid Approach: Monorepo with Multi-State #

A hybrid approach combines the monorepo advantages (single source of truth) with multi-state (isolated blast radius).

# Monorepo structure with separate state per component
infrastructure/
├── modules/            # Shared modules (monorepo advantage)
│   ├── networking/
│   ├── compute/
│   └── database/
├── environments/
│   ├── dev/
│   │   ├── networking/  # Separate state
│   │   ├── compute/
│   │   └── database/
│   ├── staging/
│   └── production/
└── .github/workflows/  # Shared CI/CD config
flowchart TD
    subgraph REPO["Monorepo"]
        M["Shared Modules"]
        E1["dev/networking"]
        E2["dev/compute"]
        E3["prod/networking"]
        E4["prod/compute"]
    end

    subgraph STATE["Separate States"]
        S1["state: dev-networking"]
        S2["state: dev-compute"]
        S3["state: prod-networking"]
        S4["state: prod-compute"]
    end

    E1 --> S1
    E2 --> S2
    E3 --> S3
    E4 --> S4

    style REPO fill:#e3f2fd,stroke:#1565c0
    style STATE fill:#fff3e0,stroke:#e65100

Monorepo CI/CD Challenges #

# Problem: building all modules on a small change
# Solution: detect changed paths and only build what changed

# GitHub Actions: path filters
on:
  push:
    paths:
      - 'infrastructure/networking/**'
# GitHub Actions: matrix strategy per component
jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      networking: ${{ steps.changes.outputs.networking }}
      compute: ${{ steps.changes.outputs.compute }}
    steps:
      - uses: dorny/paths-filter@v2
        id: changes
        with:
          filters: |
            networking:
              - 'infrastructure/networking/**'
            compute:
              - 'infrastructure/compute/**'            

  plan-networking:
    needs: detect-changes
    if: needs.detect-changes.outputs.networking == 'true'
    runs-on: ubuntu-latest
    steps:
      - run: cd infrastructure/networking && terraform plan

Shared Module Library #

# Monorepo: one repo for all modules and environments
infrastructure/
├── modules/                # SHARED: all environments use these
│   ├── networking/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── compute/
│   └── database/
├── environments/           # Per-environment config
│   ├── dev/
│   │   └── main.tf         # → source = "../../modules/networking"
│   ├── staging/
│   └── production/
└── .github/workflows/      # SHARED CI/CD config
flowchart TD
    subgraph REPO["Monorepo"]
        MOD["modules/\n(SHARED)"]
        DEV["environments/dev"]
        STG["environments/staging"]
        PRD["environments/production"]
    end
    
    MOD --> DEV
    MOD --> STG
    MOD --> PRD
    
    style MOD fill:#e3f2fd,stroke:#1565c0
    style PRD fill:#e8f5e9,stroke:#2e7d32

Summary #

  • There’s no universally better choice — a monorepo is better for small teams and tightly coupled infrastructure; multirepo is better for large teams with clear ownership.
  • Start with a monorepo if you’re just beginning — it’s easier to split later as needed, but hard to merge existing multirepos.
  • Multirepo requires module versioning — modules in separate repos must have explicit versioning so callers can upgrade gradually.
  • A per-domain hybrid is the sweet spot for medium teams — one repo per domain (networking, platform, app) with a separate repo for shared modules.
  • CODEOWNERS in GitHub/GitLab helps enforce who must review changes in specific directories — important for monorepos managed by many teams.
  • .terraform.lock.hcl must be committed in both approaches — it ensures everyone and every pipeline uses the exact same provider versions.

← Previous: Common Security Mistakes   Next: Lifecycle Management →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact