Table of contents
- What makes monorepos challenging
- Trunk-based development
- Release branches
- Independent service releases
- Versioning and dependency isolation
- Optimizing CI/CD pipelines
- Merge queues and Graphite
- Choosing the right tools
- Conclusion
What makes monorepos challenging
A monorepo houses multiple projects in a single repository. This setup simplifies code sharing and streamlines tooling but creates complexity in release management. Common challenges include high change volume, cross-team coordination, CI/CD bottlenecks, and maintaining a stable main branch.
Trunk-based development
Trunk-based development involves merging small, frequent changes directly into a central branch (e.g. main
) and keeping it deployable at all times. Teams use short-lived feature branches or commit directly to trunk, supported by a fast and reliable CI pipeline.
For monorepos, TBD reduces integration overhead. Instead of coordinating across multiple teams before merging, everyone works on the latest code. Feature flags help merge incomplete features safely. This model requires strong test automation and continuous integration discipline.
Release branches
Release branches are cut from trunk at specific points and used to stabilize code for deployment. This approach gives teams a freeze window for final QA and bug fixes without blocking ongoing development on trunk.
They're ideal for scheduled or coordinated releases. However, they require careful backporting and merging strategies to avoid drift between branches. In a monorepo, this can get tricky due to overlapping changes across multiple services.
Independent service releases
In monorepos with many services, it's common to treat each as its own release unit. Each service has its own versioning, deployment pipeline, and potentially CI triggers that activate only when its directory is modified.
This setup requires clear boundaries between projects and a way to handle internal dependencies. Teams often use tools like Lerna, Yarn workspaces, or language-specific build systems to isolate and version components independently.
Versioning and dependency isolation
Some monorepos operate without internal versioning—building everything from trunk (e.g. Google). Others version each package independently using semantic versioning. Tools like release-please
, Changesets, or Lerna help automate version bumps and changelogs based on commit messages.
To manage shared dependencies, monorepo teams often use internal package managers (npm workspaces, Gradle submodules, etc.) and enforce explicit dependency declarations. This reduces accidental coupling and helps teams adopt new versions on their own timeline.
Optimizing CI/CD pipelines
Running all builds and tests on every commit doesn't scale. Monorepos need smarter pipelines that support:
- Selective builds: Only build/test the affected projects (using path filters or dependency graphs).
- Caching: Reuse results from unchanged components.
- Parallelism: Split builds/tests across multiple workers.
- Conditional deployments: Only deploy services that changed.
CI tools like GitHub Actions, Buildkite, and Bazel support these patterns. Trunk stability is key, so merge queues are increasingly essential.
Merge queues and Graphite
Merge queues protect trunk from breaking changes by serializing PR merges. When a PR is queued, it's rebased on the latest trunk, tested in that state, and only merged if all checks pass. This avoids "green PRs" breaking when merged simultaneously.
Graphite enhances this with:
- Stacked PRs: Developers can break large changes into dependent PRs (e.g., DB migration → API → frontend). Each PR is reviewed separately, and Graphite keeps them in sync automatically.
- Optimized merge queue: Graphite tests stacks as a unit and merges them in batch if safe. This reduces CI load and speeds up integration.
- Auto-rebasing: Graphite continuously rebases queued PRs on the latest trunk, avoiding merge conflicts before they hit main.
This model suits high-throughput monorepos. Developers can push large changes incrementally, get fast feedback, and trust that trunk will remain stable.
Choosing the right tools
Here's a quick breakdown of useful tools by category:
Category | Examples | Role in monorepo management |
---|---|---|
Build systems | Bazel, Pants, Nx | Incremental builds, DAG-based test selection |
Versioning | Lerna, Changesets, release-please | Automate package versioning |
CI/CD | GitHub Actions, Buildkite | Path-based pipelines, parallelism |
Merge queues | Graphite, GitHub Merge Queue | Serialized merges, test gating |
Dependency bots | Dependabot, Renovate | Automate version bumps |
Conclusion
Releasing software from a monorepo requires intentional strategy. Trunk-based development accelerates integration. Release branches offer stability when needed. Independent service releases give teams flexibility. Versioning and CI/CD must scale with your repo.
Tools like Graphite help maintain a stable main branch and manage complex workflows with stacked PRs and efficient merge queues. Whether you’re coordinating a massive deployment or shipping a small change, the right strategy and tooling can make monorepos a powerful foundation for fast, reliable releases.