Post

Build tooling

Build tooling

ADR: Choosing Bazel over Gradle for Building Go Applications

Status: Accepted

Context

Our collaborators build and maintain several Go-based services, many of which share dependencies and communicate via protobuf-based APIs. These services are part of a larger ecosystem that includes components written in other languages, such as Java and TypeScript. To improve build consistency, speed, and reproducibility across projects, we evaluated multiple build systems for long-term standardization.

After assessing Bazel and Gradle (with Go support through community plugins), we decided to adopt Bazel as our primary build system for Go applications. The decision was driven by the need for hermetic builds, deterministic outputs, efficient caching, and first-class support for protobuf and gRPC integration.

Decision

The team will use Bazel as the standard build system for Go projects. We will rely on rules_go for Go integration, rules_proto for protobuf generation, and Gazelle for automatic BUILD file generation. Gradle and related Go plugins will not be used for production builds.

Consequences

  • Advantages

    • Reproducible and isolated builds: Bazel’s sandboxing model ensures that builds are consistent across environments and reproducible over time.
    • Controlled toolchains: With rules_go, the Go compiler and dependencies are versioned and pinned, removing discrepancies between local and CI environments.
    • Scalable build performance: Bazel’s remote caching and execution capabilities reduce build and test durations, especially in CI pipelines.
    • Support for multi-language repositories: Bazel provides a unified way to build Go, Java, TypeScript, and protobuf artifacts within a single repository.
    • Automated build metadata management: Gazelle keeps BUILD files in sync with source structure and module definitions.
    • Protobuf integration: Using rules_proto and rules_go, teams can define .proto contracts once and generate consistent server and client code. This approach standardizes server-to-server communication, ensures type safety, and simplifies maintaining cross-service APIs.
  • Trade-offs

    • Learning curve: Engineers will need time to become comfortable with Bazel’s concepts, including WORKSPACE/BUILD files and rule definitions.
    • Integration complexity: Some third-party Go modules may require additional configuration for Bazel compatibility.
    • Build verbosity: BUILD files and rules can feel more complex compared to a simple go build command or small Gradle script.

Alternatives Considered

  1. Gradle with Go Plugins (e.g., Gogradle)
  • Pros: Familiar syntax for teams already using Gradle; flexible scripting; native integration with other Gradle tasks.
  • Cons: Plugins for Go are community-maintained with inconsistent activity levels; lack of hermetic builds and reproducible toolchains; weaker support for large-scale or multi-language projects.
  1. Native Go Tooling (go build with modules)
  • Pros: Simple and idiomatic for Go developers; minimal external tooling.
  • Cons: No native support for remote caching or hermetic builds; harder to integrate consistently across polyglot repositories; more complex to maintain large-scale build pipelines.

Rationale

The decision to adopt Bazel was based on the following priorities:

  1. Hermetic, reproducible builds Bazel’s sandboxed execution guarantees that builds only depend on declared inputs. This eliminates environment drift and ensures consistency between developer machines and CI agents.

  2. Deterministic toolchain management rules_go allows us to fix the Go toolchain version and dependency graph within Bazel, producing identical binaries across environments.

  3. Remote caching and execution Bazel’s native caching and distributed execution significantly reduce build and test times across large teams and CI environments.

  4. Multi-language and monorepo compatibility Bazel supports different languages under a unified build graph. This reduces friction when combining Go, protobufs, and frontend code within the same repository.

  5. Protobuf and gRPC integration With rules_proto and rules_go, service contracts defined in .proto files are shared between teams and languages. The same definitions can generate Go clients, servers, and gRPC stubs, standardizing communication between services and minimizing integration issues.

  6. Automated build maintenance Gazelle automatically generates and updates BUILD files based on source structure, reducing manual upkeep.

  7. Mature ecosystem The rules_go, rules_proto, and Gazelle projects are actively maintained and widely used in production environments, providing stability and community support.

Why Not Gradle?

Gradle’s Go support depends on third-party plugins that vary in quality and maintenance. These plugins lack the deep integration and hermetic guarantees provided by Bazel. While Gradle offers an expressive build language and good support for Java ecosystems, its model is not optimized for reproducible, sandboxed builds across multiple languages.

Migration Plan

  1. Proof of Concept: Migrate a small service to Bazel using rules_go, rules_proto, and Gazelle to verify reproducibility, caching, and protobuf integration.
  2. Pilot Training: Provide documentation and training sessions to familiarize engineers with Bazel conventions, workspace setup, and common rules.
  3. Incremental Rollout: Gradually migrate services while maintaining Gradle or native Go builds during the transition period.
  4. CI Integration: Configure and optimize Bazel’s remote cache and execution environment for production pipelines.

Operational Considerations

  • Provide templates, example WORKSPACE and BUILD files, and editor support to ease adoption.
  • Establish standards for managing dependencies and regenerating BUILD files with Gazelle.
  • Track known issues and patterns related to rules_go and rules_proto to help teams troubleshoot consistently.

Appendices

Diagrams

High-Level Build Flow (Bazel)

flowchart LR
  subgraph Developer
    A[Source: pkg/, cmd/, proto/, go.mod] --> B[Gazelle]
    B --> C[BUILD.bazel]
  end
  subgraph Bazel
    C --> D[Analysis Phase]
    D --> E[Execution Phase: sandboxed compile/test/proto-gen]
    E --> F[Local cache]
    E --> G[Remote cache / remote execution]
  end
  G --> CI[CI agents reuse cached artifacts]
  F --> Developer

Protobuf-Based Communication Flow

sequenceDiagram
  participant ServiceA
  participant Proto
  participant ServiceB

  ServiceA->>Proto: Define contract (service + messages)
  Proto-->>ServiceA: Generate Go server and client stubs
  Proto-->>ServiceB: Generate Go server and client stubs
  ServiceA->>ServiceB: gRPC or HTTP communication using protobuf

Alternative (Gradle + Go Plugin)

flowchart LR
  Dev[Developer sources] --> Gradle[Gradle build scripts + Go plugin]
  Gradle --> LocalGo[Invokes go tool or manages GOPATH/modules]
  LocalGo --> Artifacts
  Gradle -.-> RemoteCache[Optional Gradle cache]

Decision Log

  • 2025-11-04: Decision recorded and accepted, including protobuf integration considerations.
This post is licensed under CC BY 4.0 by the author.