Content-addressed caching and sandboxed action execution moved out of bespoke monorepo infrastructure and into the default configuration of tools a five-person team installs in an afternoon. The saving appears as a rebuild that touches only the actions whose declared inputs changed. The cost appears as a failure with no error message: an action that under-declares its inputs returns a green build from a cache entry computed against source that no longer exists.
Three tools carry that trade in different proportions: Gradle, Bazel and Make.
What Gradle, Bazel and Make each are
Gradle is a dependency-based build tool for the JVM. It resolves a graph of declared coordinates, orders tasks against that graph, and gets its configuration from a Kotlin or Groovy DSL in build.gradle.kts. Bazel is a hermetic, multi-language build tool that treats every action as a function of declared inputs and declared outputs; the function form is what lets one machine reuse another machine's work without trusting it. Make is a task-based tool that compares file modification times against a rule list and runs shell recipes for any target older than its prerequisites.
The six categories of build tool documented by GeeksforGeeks place these three in three different groups: task-based, dependency-based, and multi-language declarative. A project that changes group after adoption rewrites its build, which is why the choice belongs before the first commit.
Seven attributes, one value in each cell
The category scheme behind the table comes from GeeksforGeeks; the values are the behaviour each tool provides by default.
| Attribute | Gradle | Bazel | Make |
|---|---|---|---|
| Unit checked before rebuilding | Task | Action | Target file |
| Dependency declared as | Coordinate plus plugin | Hand-written edge in a BUILD file | Prerequisite filename |
| Configuration language | Kotlin or Groovy DSL | Starlark | Makefile syntax and shell |
| Sandbox around each action | No | Yes | No |
| Shared cache across machines | Build cache, opt in | Remote cache and remote execution | None in the tool |
| Languages served | JVM first, others by plugin | Many, by rule set | Any toolchain on the shell path |
| Characteristic failure | Stale cache entry after an undeclared input changes | Compile failure inside a sandbox when a file was never declared | Stale object linked into a green build |
What each tool counts as a dependency
Gradle counts a dependency as a coordinate, group:artifact:version, resolved through declared repositories and cached under the user's home directory. Transitive dependencies arrive without anyone asking for them, and the same graph resolves identically on a laptop and on a CI runner when both see the same repositories. Bazel counts a dependency as an edge the author wrote by hand: A target specifies sources and dependencies, and a file omitted from those two groups is not recognised by the action. Counts is a dependency as a filename to the left of a colon on a prerequisite line.
The three models fail in three places. Gradle fails when a repository is unreachable or when two modules demand incompatible versions of one library, and the resolution strategy either upgrades the loser or ends the build. Bazel fails when a header is used but not declared, and the error surfaces as a compile failure inside a sandbox that cannot see the file, which is louder and earlier than a cache miss. Make fails quietly: an omitted prerequisite produces a successful link against an object file compiled from a header that has since changed.
The dependency graph anatomy GeeksforGeeks describes is common to all three, a directed acyclic graph that fixes execution order and exposes what can run in parallel. The difference lies in who fills it in. Gradle derives most of it from resolved coordinates, Bazel refuses to guess at any edge, and a Makefile contains exactly what its author typed.
The unit of incrementality
Gradle marks work up to date per task, hashing a task's declared inputs and outputs and skipping the task when the hash matches, with the build cache extending that decision across machines once org.gradle.caching is set to true. Bazel marks work up to date per action, where an action is a single compiler or linker invocation, and it hashes the full input set of that action including the contents of every declared header. Make marks work up to date per target file and compares modification times.
A Makefile makes the granularity visible:
app: main.o util.o
cc -o app main.o util.o
main.o: main.c
cc -c main.c
Change util.h and make reports that app is up to date, because no rule names the header. The same project in Bazel keeps the header in the library's hdrs attribute, so every action that uses it includes it in the input set and the hash moves:
cc_library(
name = "util",
srcs = ["util.c"],
hdrs = ["util.h"],
)
cc_binary(
name = "app",
srcs = ["main.c"],
deps = [":util"],
)
Gradle is positioned between the two. It tracks headers through the compile task's source and classpath inputs; the build script contains no per-header declaration, so it handles util.h correctly by default and mis-handles project-specific inputs, which is the gap the untracked-input warning was added to catch.
Sandboxing, and what the shared cache depends on
Bazel runs actions in a sandbox with no access to files the action did not declare, so the declared input set is the set the action can read, and a remote cache stays safe to share across machines and across repositories. Gradle has no sandbox around tasks by default, so its cache correctness rests on each task declaring its inputs honestly, and a plugin that accesses an environment variable without declaring it produces a cache hit against the wrong environment. Make has neither a sandbox nor a cache beyond the file system it runs on, so its reuse ends at build outputs already located inside the working tree.
Configuration surface, and who can read it
Gradle compiles its Kotlin DSL, which means a build script that breaks fails at configuration time with a type error, and an IDE can complete task and dependency names. Bazel restricts its Starlark files to a hermetic subset of Python with no file system access during analysis, which makes builds of the build reproducible on any machine. Make executes shell, the widest surface and the least restricted: a recipe can clone a repository, write over the network and delete a directory, and Make will report success.
tasks.register<Test>("integrationTest") {
useJUnitPlatform()
maxParallelForks = 4
}
That Gradle block is type-checked code. Its equivalent in a Makefile is a shell line, and its equivalent in Bazel is a rule definition plus a test target, and each has the diagnostic quality of the language underneath it.
Operating cost, measured in CI minutes and onboarding days
Adoption numbers give the shape of the field. The BrowserStack build automation guide reports that Jenkins has around 200,000 known installations, which makes it the default assumption inside any organisation that already runs a CI server, and that Travis CI covers over 700,000 users, a population that values a clean virtual machine per build over a long configuration surface. Perforce reports that Warhorse Studios sends automated builds to nine TeamCity build servers, a fan-out that pays for itself once the build queue contains more work than one server can drain.
Two numbers decide adoption for a small team: The minutes a cold build occupies a runner, and the days an engineer needs before the build file is a document that engineer can edit alone.
Gradle spends its minutes on a warm JVM. A daemon keeps the compiler and the plugin classpath between builds, so the second build of a morning costs less than the first, and a CI job that starts from a clean container pays the first-build price every time. Setting org.gradle.caching to true in gradle.properties returns a result stored by another machine for every task whose declared inputs hash to the same value. The onboarding days go into the Kotlin DSL, which the compiler checks and the IDE completes.
Bazel spends its minutes on process spawns and its days on the BUILD files. Each action is a fresh process inside a sandbox, which costs more per compilation than a persistent daemon, and a remote cache or a remote execution cluster returns that whole cost the first time one action repeats. The onboarding cost lands once per repository: a header the compiler finds and the BUILD file does not name produces a failure inside the sandbox on the machine of the person who wrote the missing edge, and that person is the one who fixes it.
Make is free to install. GeeksforGeeks notes that Make is universal and installed on almost every Unix system, which is the property that keeps it in Dockerfiles and in the first hour of work on a new toolchain. What it spends instead is the maintenance of an implicit graph, and the characteristic failure in the table above shows the shape of that cost: an undeclared header, a green build, and an object file compiled from last week's source.
JetBrains's CI/CD guide places the build on a dedicated server, on the grounds that a clean environment flags a missing dependency at the build stage, and recommends a failure condition for builds that do not complete inside a period the team sets, which prevents one long build from occupying a queue. The same guide publishes the artifact to a repository under a version, so the deployment stage fetches the exact build that passed testing.
The LOW/CODE account of business process automation reports that teams begin with a small pilot that tests the tooling and measures the impact before a full rollout, and identifies high initial implementation cost as the ninth of its sixteen common challenges, which is the spend the pilot prices while one repository is still the exposure.
What a shared cache requires of the build underneath it
A shared cache repays its setup on a build that already passes from a clean checkout. A cache stores the result of an action under the inputs that action declared, so an action that accesses a file it never declared keeps a stored result under a key the next checkout reproduces, and the build reports success against source it no longer touches. GeeksforGeeks mentions that Bazel is chosen for hermetic builds inside isolated sandboxes and extreme caching, and the sandbox contains a cached result to the inputs the action declared.
Which tool to install, at which size of repository
Three questions settle the choice for most repositories. A project that compiles more than one language takes Bazel, because the rule sets already exist for Java, C++, Go and Python and the hermetic graph is the point of the tool. A team that wants the build file checked by the compiler and completed by the IDE takes Gradle, which type-checks the Kotlin DSL at configuration time. A repository with one language, one runner and a build under five minutes takes Make, which is present on the host and needs no install step.
GeeksforGeeks's selection guidance arrives at the same split: small scripts go to Make or to a package runner, and enterprise systems go to a dependency-based or declarative tool. The threshold underneath that advice is the number of edges in the dependency graph. A graph small enough to hold in one head survives a file of shell recipes; a graph that resolves transitively and caches across machines needs a tool that computes the order itself.





