Skip to content

Trunk

Introduction

Repository organization is a foundational decision that affects how teams collaborate, how code is versioned, and how deployable modules are structured.

This article explains the two primary patterns - monorepo and polyrepo - and provides guidance on choosing the right approach.


Monorepo Pattern

Monorepo Structure

A single repository containing more than one deployable module.

Path filters (glob patterns) define module boundaries, allowing independent versioning and deployment despite sharing the same repository.

Characteristics

Single Repository:

  • All application code in one repository
  • Shared infrastructure and tooling
  • Unified version history
  • Single source of truth

Code Organization:

  • Organized by technology first, then module directories
  • Shared tooling and configuration
  • Unified dependency management

Example Structure:

monorepo/
├── services/       # module group (e.g., dotnet)
│   ├── api/        # deployable module
│   ├── web/        # deployable module
│   └── worker/     # deployable module
├── shared/         # module group
│   ├── models/     # supporting module (implicit consume)
│   └── utils/      # supporting module (implicit consume)
├── infrastructure/ # IaC (e.g., Bicep)
└── docs/           # documentation (e.g., mkdocs)

Benefits

  • Atomic Changes: Refactor multiple services in single commit
  • Simplified Dependencies: Shared dependencies at root level, no version conflicts
  • Code Sharing: Direct access to shared utilities and libraries
  • Unified Tooling: Single CI/CD configuration, consistent build process
  • Easier Refactoring: Cross-module changes in one PR

Tradeoffs

  • Build Times: Need selective builds and caching strategies
  • Access Control: Coarser-grained permissions, all developers see all code
  • Repository Size: Can grow large, may need specialized tooling
  • Cognitive Overhead: Developers may be overwhelmed by scope

When to Use

  • Team-coupled services maintained together
  • Heavy code reuse across projects
  • Small to medium teams (< 50-100 developers)
  • Rapid iteration requiring frequent cross-cutting changes
  • Unified ownership of all code

Polyrepo Pattern

Polyrepo Structure

One repository per deployable module.

The repository boundary aligns perfectly with the module boundary, making versioning straightforward.

Characteristics

Multiple Repositories:

  • One repository per service or component
  • Independent version history
  • Separate access controls
  • Isolated tooling and configuration

Code Organization:

  • Each repository is self-contained
  • Dependencies managed per repository
  • Service-specific configuration

Example Structure:

api-service-repo/
├── .github/          # pipeline
├── api-service/      # deployable module
├── infrastructure/   # IaC
└── docs/             # documentation
shared-models-repo/
├── .github/          # pipeline
├── shared-models/    # deployable module (explicit consume via package)
└── docs/             # documentation

Benefits

  • Team Autonomy: Teams own and control their repositories
  • Clear Boundaries: Enforces service boundaries, prevents unintended coupling
  • Independent Deployment: Services deploy on separate schedules
  • Granular Access Control: Fine-grained permissions per repository
  • Smaller Repositories: Faster clone times, simpler navigation

Tradeoffs

  • Cross-Repository Changes: Multiple PRs needed, coordination required
  • Dependency Management: Version conflicts between repositories, contract testing required
  • Tooling Duplication: CI/CD configuration duplicated across repos
  • Discoverability: Harder to find related code, no unified search

When to Use

  • Loosely coupled services with independent lifecycles
  • Large organizations with separate team ownership
  • Distributed teams with confidentiality needs
  • Independent deployment cadences
  • Well-defined APIs between services

Repository Types

Repository Types

Poly-repository: One deployable module. Repository boundary = module boundary.

Mono-repository subtypes:

  • Team mono: Single team owns multiple modules
  • Product mono: Multiple teams collaborate on one product
  • Organizational mono: Large-scale (Google/Facebook style, not recommended for most)

Adjacent Repositories

Not all repositories are trunks. Adjacent repositories serve specialized purposes beyond containing deployable modules.

Example: GitOps Repository

A Kubernetes GitOps repository is a special form of IaC constrained to declarations only. It cannot easily be integrated into a trunk, nor should it be: the GitOps repository's history should be the history of the cluster it controls, one-to-one.

Summary

Repository Type Purpose Contains Modules?
Trunk (mono/poly) Source of truth for deployable code Yes
GitOps repo Cluster state declarations No
Config repo Environment-specific configuration No
Template repo Repository scaffolding No

Adjacent repositories are consumed by or triggered from trunk pipelines, but maintain their own lifecycle and history.

Comparison

Mermaid diagram

Factor Monorepo Polyrepo
Atomic cross-cutting changes Excellent Difficult
Traceability Single timeline Multiple timelines
Team autonomy Limited High
Pipeline complexity Requires specialized tooling Simple
Coordinated releases Simple Harder
Code reuse Easy (direct consume) Requires versioning
Access control Coarse-grained Fine-grained
Discoverability All in one place Spread across repos

Anti-Pattern: Technical Boundary Split

Repository Anti-Pattern

Splitting repositories by technical boundary (frontend/, backend/, scripts/, infrastructure/) rather than deployable module boundary creates dependency chaos.

Problems:

  • No coherent version for what a release consists of
  • Changes to one feature require coordinating multiple PR's and repositories
  • No clear deployment boundary

Avoid:

  • Separate repos for frontend, backend, scripts, docs, infrastructure unless each is a distinct deployable module with proper contracts
  • Loose gatherings of repositories with cross-repository dependencies
  • Head-to-head dependencies between repos (use pinning and stitching instead)

Best Practices

Module Boundaries: Define explicit boundaries, document dependencies, enforce with tooling.

Versioning:

  • Monorepo: Independent SemVer/CalVer for each releasable module
  • Polyrepo: One SemVer/CalVer per repository (can still use implicit for supporting modules)

Dependency Management:

  • Root-level lock files for reproducible builds:
Ecosystem Lock File Notes
Go go.sum Checksums for module dependencies
Node.js package-lock.json Or yarn.lock, pnpm-lock.yaml
Rust Cargo.lock Exact versions for reproducible builds
Ruby Gemfile.lock Resolved gem versions
.NET packages.lock.json NuGet package lock (opt-in)
Python poetry.lock Or requirements.txt with pinned versions
Java gradle.lockfile Or Maven's dependency-lock.json

Hard dependency rules:

  • In-repository bindings are implicit
  • Cross-repository bindings are pinned and stitched
  • No exceptions: every dependency is either implicit (same repo) or explicitly pinned (external)

Automation:

  • Monorepo: Change detection, selective builds tooling (r2r+eac, Nx, Bazel, Turborepo)
  • Both: Build, Test, Scans, Verification, Repository templates, shared pipeline definitions, Dependabot

Next Steps

References


Tutorials | How-to Guides | Explanation | Reference

You are here: Explanation — understanding-oriented discussion that clarifies concepts.