By clicking “Accept”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts. View our Privacy Policy for more information.
18px_cookie
e-remove

5 Tips for Managing Bazel Dependencies (Without Losing Friends)

Upgrading dependencies in a Bazel monorepo? Learn 5 tips to avoid breakages, reduce risk, and keep your team (and builds) running smoothly.

Upgrading dependencies in a Bazel monorepo? Learn 5 tips to avoid breakages, reduce risk, and keep your team (and builds) running smoothly.

Upgrading dependencies in a Bazel monorepo? Learn 5 tips to avoid breakages, reduce risk, and keep your team (and builds) running smoothly.

Written by
A photo of Alexandre Wilhelm — Senior Software Engineer at Endor Labs.
Alexandre Wilhelm
Published on
May 12, 2025

Upgrading dependencies in a Bazel monorepo? Learn 5 tips to avoid breakages, reduce risk, and keep your team (and builds) running smoothly.

Upgrading dependencies in a Bazel monorepo? Learn 5 tips to avoid breakages, reduce risk, and keep your team (and builds) running smoothly.

At Endor Labs, we work with large Bazel monorepos every day — including our own. Here’s what we’ve learned about updating dependencies without breaking everything (or everyone).

Like most others leveraging Bazel, we picked a monorepo approach to development to have a single common way to build software and full visibility across our codebase. We use Bazel across multiple languages and with a fast scaling team we need a way for our builds to scale. More on why we picked the path of the monorepo can be found here.

Bazel manages dependencies in a hermetic, explicit, and centralized way, which ensures that every buildable unit of software, otherwise known as target, uses the same version of a dependency unless you go out of your way to support multiple versions. This ensures consistency and avoids “it works on my service” issues.

The benefits of this approach are:

  • Safer updates: Any version bump has an obvious blast radius.
  • Predictable builds: You won’t have subtle version drift.
  • Faster builds: Same inputs = same outputs = faster builds from faster cache referencing

However the drawbacks of this approach are:

  • Additional update complexity: Additional time required to migrate all services.
  • All services must update at once: There is no such thing as a “quick fix” for just one service
  • Updates of foundational services: May potentially be “large lifts”.

Updating a dependency might seem like a routine task. But in a Bazel monorepo, it’s a change that potentially affects every corner of your codebase. A single library bump can trigger unexpected breakages across hundreds of other dependent pieces of software. And without a solid process, you can quickly find yourself in a tangle of failed builds, subtle test regressions, and half-rolled upgrades.

Done right, updates help you fix vulnerabilities, adopt new features, and keep your supply chain healthy. Done wrong, they introduce risk, tech debt, and late-night Slack pings.

So we’ve come up with 5 tips and tricks to help you not lose friends when managing software component updates in a Bazel environment:

  1. Embrace the expanded visibility into your code consumers
  2. Prefer single-dependency updates to minimize blast radius and simplify debugging
  3. Avoid batch upgrades across multiple ecosystems; break large upgrades into smaller, logical steps
  4. Make incremental testing core to your testing strategy
  5. Focus security scanning on binary targets

1) Embrace the expanded visibility into your code consumers

In a Bazel monorepo, a single change is visible across the whole repository immediately. That’s a feature, not a bug. Sure, it will take you longer to fix a problem, correctly. Would you rather realize a week later that something was broken or proactively manage it? The ability to proactively manage the impact of changes is a core feature of a monorepo. 

If your update breaks something, the build will fail. Not for some downstream consumer — for you.

There’s no hiding behind versioning. No shipping broken code into another team’s repo. If you’re updating the dependency, you also own fixing what it breaks. That might feel like more work upfront,  but it saves a lot of pain later. It's important for teams to embrace this fluctuation.

Part of embracing this might be making central dependency updates a team wide concern rather than the responsibility of an individual service owner driving a change. At a large scale this may be the prudent approach. Having a platform team or DevOps team own updates can help drive resolution holistically.

2) Prefer single-dependency updates to minimize blast radius and simplify debugging

When you have dozens of languages, thousands of targets, and multiple teams working in the same repo, things can get out of hand quickly.

Dependency updates are like any other code change: smaller is safer. When you update a single dependency at a time, you keep the scope tight, the diff readable, and the blast radius controlled. 

In a Bazel monorepo, a single change is visible across the whole repository immediately — which means you’ll find out right away if it breaks something. But it also means you should keep the change as narrow as possible to make it easy to fix, reason about, or revert if needed.

Batch updates across libraries, languages, or services might seem efficient, but they make debugging painful. If the build fails or tests regress, you’ll have a hard time pinpointing which change was responsible. Testing “everything” isn't always realistic. But testing the right things with the right tools is!

3) Avoid batch upgrades across multiple services; break large upgrades into smaller, logical steps

Cross-cutting updates (i.e. to foundational libraries such as protobuf) might be unavoidable — but they shouldn’t be done all at once.

Instead, break the work into steps. Update one set of services or consumers at a time. Submit a series of PRs with scoped fixes and clear test boundaries to a branch that addresses all issues

This staged approach makes rollbacks safer and gives you a much better signal on where things are going wrong. It also allows teams to unblock incrementally — which is critical in fast-moving environments where you can’t afford to hold the whole org hostage waiting for a perfect green build.

4) Make incremental testing core to your testing strategy

If your monorepo is small — great. Run all the tests. Don’t overthink it.

Dependency updates aren’t a daily task, so bite the bullet and run everything. It’s the safest move, and the time cost is usually worth the confidence it gives you.

But once your repo scales, you need to be smarter. Bazel gives you the tools to do that with bazel query.

Let’s say you’re updating the zap logging library for your golang services in `src/golang/`. Instead of running your entire test suite, you can use Bazel to identify only the targets that are impacted by the change.

Start by querying all reverse dependencies of the zap library:

bazel query 'kind("go_test", rdeps(//src/golang/..., @org_uber_go_zap//:zap))'

Then pass that list to the test command:

bazel test $(bazel query 'kind("go_test", rdeps(//src/golang/..., @org_uber_go_zap//:zap))')

This ensures you're testing both direct and transitive consumers of the dependency — catching real issues without wasting CI cycles on unrelated code.

If you want to go even further and narrow the scope, Bazel lets you control the depth of the dependency graph. For example:

bazel test $(bazel query 'kind("go_test", rdeps(//src/golang/..., @org_uber_go_zap//:zap, 2))')

This limits the query to only two levels of reverse dependencies. It’s a useful way to cut down test time when you're confident that deeper transitive paths are unlikely to be affected (e.g. for a minor patch version or non-breaking change).

The more you understand your dependency graph, the more surgical your tests can be — and the faster your team can move without compromising safety. That being said, not all tests are created equal — and not all of them are meant to run on your laptop.

  • Unit tests are typically fast, hermetic, and runnable locally with minimal setup. These are your first line of defense when updating a dependency.
  • Integration tests, on the other hand, may require databases, services, or full environments that only exist in CI. These tests are often slower, more expensive to run, and harder to replicate locally.

That’s where your CI pipeline comes in. Most Bazel monorepos are set up so that CI only runs the tests affected by your change. That includes updates to third-party libraries — even if you didn’t change any of your own code — because those updates still affect the dependency graph.

When running security scans in CI/CD pipelines with a Bazel native approach, it is not necessary to scan every target on every run. To optimize performance and avoid redundant analysis, you can scan only the targets that are affected by recent changes. 

This selective scanning strategy helps reduce noise, speeds up the pipeline, and ensures security analysis remains focused on what's actually being developed or deployed.

5) Focus security scanning on binary targets

Why do you generally update software dependencies? Most often it’s because something is broken, because you can take advantage of new features or for security updates. With security updates being a large driver for the average team to update their software, managing them becomes crucial. You don’t want to focus your team's time and attention on large updates where you don’t even use the vulnerable functionality and you also want to focus on critical targets. 

Scanning the entire monorepo means analyzing all declared dependencies across all targets, regardless of whether they are actually used by a particular application or service or tests. This can lead to longer scan times and includes results that may not be relevant to the component being deployed or maintained.

In contrast, scanning individual targets—such as a specific service—focuses only on the dependencies actually used by that target. This makes scans significantly faster (since only a part of the monorepo is being scanned) and enables incremental workflows, where only impacted targets are scanned after changes. If you can scan faster then you can introduce workflows that can prevent new issues being introduced rather than auditing nightly like a n00b mortal. 

Binary targets such as go_binary, py_binary, or java_binary produce executable applications or services. They are built from one or more libraries and represent the final output that gets usually deployed or run in production.

If you don’t know what types of executable files are produced monorepo or are just starting out with Bazel, that's okay. You can get that information using Bazel itself. 

To discover all the binary output types created in your monorepo, you can use the command:

bazel query 'kind(".*_binary", //...)' --output=label_kind \

  | awk '{print $1}' \

  | sort -u

Scanning a binary target provides a complete view of the dependencies that make up the final artifact that is shippable.

  • All internal libraries (first-party code)
  • All third-party dependencies (open source packages, transitive libraries, etc.)

Conclusion

Bazel gives you control and visibility. Monorepos give you consistency. But with great power comes great upgrade complexity.

If you’re maintaining a Bazel monorepo, the key is to treat dependency updates as first-class changes. Be deliberate. Test what matters. Embrace the visibility that comes with owning the impact — and build a process that makes it manageable.

At Endor Labs, we take the approach of using a Bazel native approach to software component discovery. We know that teams are sick of using costly and ineffective means of scanning their monorepos.

The Challenge

The Solution

The Impact

Book a Demo

Book a Demo

Book a Demo

Welcome to the resistance
Oops! Something went wrong while submitting the form.

Book a Demo

Book a Demo

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Book a Demo