Monorepos often spark debate. On the one hand, they centralize code, promote reuse, and simplify dependency management. On the other, they can lead to longer build times, slower tests, complex tooling, and complicated CI pipelines as the repository grows. By leveraging Bazel, you can minimize many of these issues and maintain a healthier development flow.
In this article, we will examine common monorepo challenges and show how Bazel helps turn a large codebase into a more manageable, efficient ecosystem. Also, we included a link to a working repository where the ideas described here are implemented.
The challenge: slower builds and increasing complexity
As a monorepo grows, a small change in one service can trigger a full rebuild of the entire codebase, often due to limitations in the build system or tooling. On the other hand, languages like Scala, known for features such as higher type safety, implicit parameters, and macros, can lead to longer compilation times. While Scala's complexity increases compile time, incremental compilation can often mitigate the need for full rebuilds.
Bazel’s incremental build caching ensures that only the affected parts of the code are recompiled. For example, if you modify a utility function used by one microservice, Bazel detects that only this microservice needs rebuilding, not the entire monorepo. This dependency analysis keeps build times manageable and shortens your development feedback loop.
The challenge: difficulties with managing a shared code
One of the main reasons for adopting a monorepo is to share code—like library functions, logging frameworks, or service interfaces—across multiple projects. However, without careful handling, updating these common components can lead to inconsistencies in versions used by different services or break dependent services.
In the mentioned project’s structure, a dedicated commons directory contains shared libraries. Bazel tracks these dependencies and ensures that any change in the shared code triggers only the necessary recompilations and tests.
Instead of manually juggling versions across repositories, you can maintain a single source of truth. Service 1 and Service 2 can both depend on a shared code, knowing Bazel will handle updates consistently.
The challenge: longer test cycles
Running all tests on every code change can turn into a time sink. As services multiply, testing time increases, slowing down CI pipelines and developer workflows.
Bazel’s incremental testing allows you to run only the tests affected by your recent changes. For example, if you just tweaked the code in service-1, then only tests that must be re-run are for this service. Tests for services that were untouched by your modification remain cached. This optimization speeds up local development and supercharges your CI runs, as demonstrated in the example project where test times drop significantly after the initial run.
The challenge: slow and inefficient CI
Traditional CI systems might rebuild everything from scratch and run all the tests—even those unrelated to the commit. This inefficiency wastes resources and increases feedback time.
Bazel can dramatically reduce build and test times on each pull request by integrating caching strategies into CI workflows, as shown in the project’s GitHub Actions configuration.
Persistent caching means CI workers start from a known good state, only re-building and re-testing what’s necessary. Over time, this leads to consistently fast pipelines.
The challenge: disparate tooling for different services
A monorepo often contains scripts, configuration files, Dockerfiles, and more. Without a unifying approach, you might end up with a patchwork of tools and languages, complicating maintenance.
Bazel’s extensible rules allow you to define everything, from Scala code compilation to Docker image creation, within one coherent system. In the example project, Docker images for each service are built using rules_oci integrated directly into the Bazel BUILD files.
Similarly, scripts can be written in Scala and executed through Bazel. This consistency reduces the cognitive load on developers—no more juggling multiple build-and-run approaches.
Working project with Scala and Bazel can be found here.
Below you will find additional explanations of the project’s features.
Selective rebuilds
As an experiment, build a repo with “bazel build //…” then make a small code change in service-1 and rebuild with “bazel build //…”. You will notice that only service-1 will be recompiled. No wasted effort on service-2 or commons unless they’re affected.
It’s worth mentioning that the granularity of caching can be managed. By introducing more BUILD.bazel files, even the smaller parts of services can be built concurrently.
Tests at your fingertips
Run all the unit tests. After the first run, subsequent runs without code changes are nearly instantaneous. When you modify code in one service, only that service’s tests are re-run, while others remain cached.
Docker made easy
Creating a Docker image for service-1 is as simple as bazel run //projects/service-1/src/main:local_image. Need to push it? bazel run //projects/service-1/src/main:push. Bazel takes care of knowing when the underlying binaries need updating.
Scripts without redundant tooling
Run Scala-based scripts directly: bazel run //projects/scripts:manualInit -- arg1 arg2. They benefit from the same incremental builds and can reuse shared code, preventing the script from drifting out of sync.
Is this all we can achieve with Bazel? Definitely, not. The mentioned project is just a small template. Real production monorepos use additional features, such as:
- remote caching or remote execution
- multilanguage support (Scala, Java, GoLang, Javascript..)
- Continuous Delivery which knows what services should be deployed
- security scans
- and others
It might sound overwhelming, but tech companies know they must meet many requirements. Investing in tooling, especially automation, pays off with fewer incidents, better security, and better quality of delivered software.
Monorepos aren’t free from complexity. Without the right strategy, you risk slow builds, complicated dependencies, and unwieldy testing strategies. But as the Scala monorepo example shows, Bazel’s caching, incremental builds, selective testing, and unified tooling can mitigate many of these pain points.
The result? You get the benefits of a monorepo—like easy code sharing and unified CI—while minimizing the usual drawbacks that large codebases can introduce.
In short, leveraging Bazel transforms a monorepo from a potential maintenance headache into a streamlined, developer-friendly environment.