Monorepo build: where the team's time is lost
A monorepo starts as a convenient solution: shared libraries sit next to services, code review rules are unified, and a cross-cutting change ships as one set of changes.
The problem emerges as it grows — dozens of services and packages are connected by a shared dependency graph, and a change in one module formally affects its neighbors through shared libraries, contracts, and data schemas.
Then the "build everything" effect kicks in. The continuous integration system sees a new change in the repository and starts checking all modules: linters, tests, image builds.
A run takes dozens of minutes, and parallel branches form a queue of build tasks — agents are busy with other people's runs, a fresh commit waits for resources to free up, even though its changes affect a single package.
This is where the bottleneck of the build pipeline shows.
A long project build stops being an inconvenience and becomes a drag on the entire team: code feedback arrives hours late, the developer switches to another task, merge conflicts pile up, and a broken change is discovered when the context is already lost.
Incremental runs and caching of intermediate results smooth over the picture, but do not eliminate the cause: the pipeline does not distinguish affected from unaffected modules, so it pays for everything.
Let's break down how this is solved: by building a dependency graph between modules, selecting only affected ones, content-based caching of files, and splitting the queue by branch priority.
What building only affected modules gives you
Building affected modules relies on the repository's dependency graph: on every change, the system calculates which projects are affected — the module itself and everything that depends on it transitively.
Tools like Nx keep such a graph up to date and recalculate it from source code rather than manual lists. Everything outside the affected set is not checked.
How much time change-based checking saves
Change checking time stops depending on repository size and starts depending on the size of the edit: a change in a leaf module pulls 3-5 tasks instead of hundreds.
Build result caching amplifies the effect — if a task's inputs have not changed (source code, configuration, environment), the result is pulled from cache and the task is not executed at all. A branch with a pinpoint edit passes checks in single-digit minutes where dozens used to be spent.
How the load on build machines drops
Parallel build tasks are distributed by the graph: agents receive independent branches and do not rebuild the shared dependency prefix from scratch.
Abandoning full rebuilds removes the peak queue — instead of a hundred tasks per merge, 5-10 arrive, and freed slots go to checking several branches simultaneously.
The cache is shared across all agents: a repeated task takes a ready artifact instead of starting compilation again, so build machines stop being a bottleneck as the number of teams grows.
Build modes in a monorepo: comparing options
The choice of build mode determines how long an engineer waits after pushing changes and how many resources the build pipeline consumes. Below are the real alternatives teams choose between, and their limitations.
| Option | When it fits | Limitations |
|---|---|---|
| Full repository build | Small monorepo, infrequent commits | Time grows linearly with the number of modules |
| Selective module build | Dozens of services, frequent integration | Needs an accurate dependency graph: an error in it breaks the release |
| Build artifact caching | Stable stack, rarely changing dependencies | A cache miss returns full build time |
| Distributed build | Many independent modules, long tests | Harder debugging, isolated agents required |
| Splitting into multiple repositories | Loosely coupled domains, different teams | Atomicity of changes is lost, contracts are versioned manually |
Shared cache and dependency graph
The graph is built from module manifests, and its accuracy determines which tasks end up in the build at all. The artifact cache lives on shared storage and is reused across branches: rebuilding an unchanged module takes seconds instead of minutes.
If the graph describes implicit connections incompletely, some dependent modules are not rebuilt — and the defect surfaces in the production environment.
When splitting repositories is more profitable
Splitting is justified when domains share almost no code: then selective builds give way to independent pipelines in separate repositories. The price is that shared libraries have to be versioned and updated manually, and a cross-cutting change goes through several approvals. While the share of shared edits is high, a monorepo with affected builds is cheaper to operate.
How we implement affected-module builds
By default, a monorepo pipeline rebuilds everything. While there are a dozen modules, this is tolerable, but at a hundred packages the build stops keeping up with the pace of edits: the queue grows, and a failed test in someone else's module blocks the release. The work order below is arranged so that only affected modules and their consumers are launched.
From audit to selective build
- Build pipeline audit. We capture stage timings, note what is cached, and record metrics before changes — otherwise there will be nothing to compare the effect against later.
- Module dependency map. We analyze connections between packages, shared libraries, and circular links that cause selective runs to miss things.
- Selective build setup. We tie runs to changed paths and rules: an edit in a shared library pulls in all its consumers, and ready artifacts are reused.
- Debugging false runs. We go through edge cases — configuration changes, data schemas, transitive dependencies, when too much is built or the needed thing is skipped.
Pilot and acceptance criteria
- Pilot on one stream. We enable the mode for one team and compare the result with a full build: missed builds are visible before rolling out to the entire repository.
- Handing regulations to the team. We describe how to add a module and declare dependencies; after that, the team maintains the pipeline without us.
The acceptance criterion is no missed module that is actually affected, and only isolated extra runs. After that, queue wait time stops depending on repository size.
Artifacts and regulations that remain in the repository
The dependency graph and build launch rules work as long as they are recorded in the repository, not in chat and not in one engineer's head.
Therefore everything that describes the build sits next to the code and goes through the same review process: a change to the rules is visible in history, can be discussed, and can be rolled back together with the release.
- Build task configurations — a declarative description of steps and launch conditions: which branch we build on, which checks are mandatory, what is skipped for documentation edits.
- Dependency schema — the graph of packages and connections between them; it is used to calculate which builds and tests are affected by a change and which do not need to run.
- Code review regulations — the mandatory set of static analysis and tests with coverage thresholds before merge, the procedure for exceptions, and who approves them.
- Access policies for build agents — who is allowed to run builds, change rules, and read secrets; where keys are stored, how they are issued, and how they are revoked when a person leaves.
- Build documentation — pipeline design, where to look at logs and metrics, which failures are typical, and what to do when each stage fails.
- Developer instructions — how to reproduce the build locally, how to add a package without breaking the graph, and which branch and commit naming conventions are adopted.
- On-call regulations — who responds to a failed build, within what time, when the pipeline is stopped, and how the team is notified.
This set remains with the team after launch: it is not reconstructed from memory, and a new engineer is onboarded into the process in days, not weeks.
Case: a monorepo with dozens of modules and check time
The client came with a monorepo of several dozen modules, where every commit triggered a build and check of the entire tree.
The median check held at around an hour, the queue for build machines grew faster than the team, and developers began batching edits into large packages — just to avoid waiting for the pipeline again.
Measurements were taken twice and independently. First from GitLab CI logs — the time of each stage on the same set of changes before and after the rebuild.
Then the same commits were run on a copy of the repository and a separate executor to exclude the influence of cache and neighboring tasks. Both measurements agreed within the margin of error — the result is reproducible, not a one-off lucky run.
What exactly changed in the pipeline
- We built a module dependency graph and keep it in the repository next to the code.
- Checks run only on affected modules and those that depend on them; everything else is marked as skipped.
- The artifact cache was tied to module content rather than build number, so it is reused across branches.
- Fast checks weed out changes that definitely fail before heavy tasks.
- Parallelism was limited by the number of build machines so the queue does not eat the gain.
After implementation, the median check dropped from an hour to single-digit minutes, and builds touching less than a tenth of modules became predominant. The load on build machines dropped so much that freed capacity was given to nightly runs.
A separate class of failures that had been blamed on "flaky tests" also disappeared: failures came from shared state and cache between modules; now each task has an isolated working directory.
What to do if the build has already grown to dozens of modules
Yes, switching to selective builds in a monorepo is done without stopping development — under one condition: the selective contour is first calculated in parallel with the full run and does not affect the main branch status.
Comparing the two runs on the same change shows which modules the system considers affected and where the dependency graph lies.
Outdated modules are moved out as a separate step: removed from mandatory builds, the last working point is recorded, and they are kept as an archive branch. First, check that no live code references them — a hidden dependency surfaces precisely after exclusion.
| Step | What it affects | What we risk |
|---|---|---|
| Dependency inventory | module graph, shared libraries | incomplete graph: a library edit does not pull consumers, and the branch looks green |
| Observation | reports only, the contour does not change | discrepancy in rules for determining affected modules |
| Migrating part of the modules | path rules, intermediate artifact cache | a shared library changes — all consumers have to be rebuilt |
| Freezing outdated modules | exclusion from mandatory checks | a hidden reference from live code to a frozen one |
| Full migration | main branch, mandatory checks | loss of the safety net: a fast rollback to the full run is needed |
Each step is rolled back with one rule change, and the full run is removed last — when the selective build matches it in results. The outcome: a typical change rebuilds single-digit modules instead of dozens, and the main branch does not sit idle.
Common technical questions before starting work
Before starting, engineering teams ask roughly the same set of questions: how complete checks are with selective builds, what happens to shared libraries, and how the pipeline lives after handover. We answer briefly — to the point.
How do you make sure selective builds do not skip the needed checks?
The dependency graph is built from package manifests and imports in code, not from directory structure. Changed paths are matched to packages, and all those that depend on them transitively are included in the build.
On top of that, protected branches always run a full build — it serves as the reference against which we compare the selective result.
What happens if a shared library has changed?
A change in a shared package marks all its consumers, so they are rebuilt together with it. We handle "global" edits separately — version lock files, build configuration, and root rules: they trigger a full run because they affect the entire repository.
What if a check is still missed?
We catch false misses by comparing the list of affected packages with the full run on release branches. Every discrepancy is investigated and closed with a mapping rule or an explicit exception.
Such discrepancies are recorded in the log — it is visible that the cause is gone rather than recurring a month later.
Who maintains the pipeline after launch?
The configuration lives in the repository next to the code and is changed through ordinary review. The matching rules, cache schema, and procedure for adding a new package are described in the documentation, so the team creates services from a template without us.
Roles are separated: a developer runs builds and reads logs; changes to shared rules and secrets go through separate access and approval.
Let's discuss the task: what to clarify before starting
So that the estimate for build work is based on facts rather than assumptions, we need initial data about the repository: how many packages and applications it has, where shared libraries live, and who reuses them.
Based on this information, we calculate which checks to run only for affected parts of the code and which to always run, and prepare an implementation plan with realistic timelines.
Below is what should be clarified before starting. If some data is not yet available, we will discuss the task based on what exists: a trial run on one package usually shows the real picture faster than a full description in words.
- Repository structure: number of packages, boundaries between them, shared modules, and connections between builds.
- Branching scheme: which branches are considered main, how changes are formatted and merged.
- Current check time: how long a full run takes and how long checking one edit takes.
- The longest and most fragile checks: what fails most often and why.
- Access restrictions: rights to the repository, build nodes, and artifact storage.
- Expectations for implementation timelines: what is critical to get first and what can be postponed.
Describe the task in free form — we will come back with an estimate and work plan, without generalities.







