skip to content

What are common pitfalls and anti-patterns when using buildSrc in a large multi-project build?

level: seniorimportance: should knowfreq 40%

answer

  1. globality = build-wide blast radius
  2. no production/runtime code
  3. minimal pinned dependencies
  4. no eager cross-configuration
  5. graduate to build-logic when large

basics

~20 s

Don't dump churny or production code in buildSrc, don't let it grow huge (every change rebuilds the world), avoid eager cross-project configuration from it, and keep its dependency surface tight. For big repos, graduate to a build-logic composite build.

solid answer

~50 s

Key anti-patterns: (1) **Bloat/churn** — a large, frequently-edited buildSrc taxes every build because its output is on every build-script classpath and any change invalidates build-script/configuration caching. (2) **Putting application code there** — buildSrc is build-time logic only; runtime code belongs in real modules. (3) **Heavy, broad dependency declarations** — buildSrc's classpath leaks into build scripts; pin and minimize. (4) **Doing eager `allprojects {}`/`subprojects {}` cross-configuration from buildSrc** — it couples modules and defeats isolation/parallel configuration (a sibling topic, but a real buildSrc smell). (5) **Treating buildSrc as the only tool** — once it's large, a separate `includeBuild("build-logic")` composite build gives finer recompilation scope and explicit boundaries. The senior mindset: buildSrc is the low-ceremony default that should stay small, stable, and focused on convention plugins plus a few task types; promote to build-logic when its size/churn starts dominating build times.

code

kotlin · 10 lines
kotlin
// ANTI-PATTERN: convention plugin reaching across the whole build
// buildSrc/src/main/kotlin/acme.bad-conventions.gradle.kts
rootProject.allprojects {           // couples modules, serializes config
    apply(plugin = "java")
}

// BETTER: configure only THIS project; let each module apply the convention
// buildSrc/src/main/kotlin/acme.java-conventions.gradle.kts
plugins { `java-library` }
java { toolchain { languageVersion.set(JavaLanguageVersion.of(21)) } }

go deeper

for a junior

Know buildSrc is build logic, not app code, and shouldn't grow unbounded.

for a middle

List the main pitfalls: bloat, churn, runtime code, heavy dependencies.

for a senior

Reason about blast radius, configuration coupling, and when to migrate to a build-logic composite build with measurements.

for a principal

Set org policy for build-platform structure, churn budgets, dependency governance, and migration thresholds.

## The trap: convenience scales badly buildSrc is wonderfully low-ceremony — write a class, use it everywhere. That very globality is the source of every pitfall: its output is on **every** build script's classpath, so its size, churn, and dependency surface all have a build-wide blast radius. ## Anti-patterns ### 1. Bloated, churny buildSrc If many engineers edit a large buildSrc constantly, every edit recompiles buildSrc and busts the build-script/configuration cache for the next build across all modules. Symptom: "why does an unrelated edit re-run configuration?" Fix: keep buildSrc small and stable; move volatile experimentation out. ### 2. Production code in buildSrc buildSrc compiles into the build, not your application. Putting shared *runtime* utilities there is a category error — they won't be on the application classpath and they pollute the build's classpath. Runtime code belongs in a normal library module. ### 3. Unbounded dependency surface Whatever you declare in `buildSrc/build.gradle.kts` ends up on the build-script classpath. Pulling in heavy or version-unpinned libraries can cause classpath conflicts in build scripts and slow buildSrc compilation. Keep dependencies minimal and pinned (ideally via a version catalog — a sibling topic). ### 4. Eager cross-configuration from buildSrc A convention plugin that does `target.rootProject.allprojects { ... }` or reaches across modules eagerly recreates the isolation problems of `allprojects {}` (a sibling topic) — it serializes configuration and couples modules. Convention plugins should configure **the project they're applied to**, not reach outward. ### 5. Over-reliance instead of build-logic When buildSrc grows into a real plugin codebase, the single-JAR, all-or-nothing recompilation hurts. A separate `includeBuild("build-logic")` composite build lets you split plugins into focused modules with their own boundaries (the distinction is its own sibling topic) — but recognizing *when* you've outgrown buildSrc is a senior judgment call. ## Healthy buildSrc checklist ``` [x] Small: a handful of convention plugins + a few task types [x] Stable: edited rarely, reviewed carefully [x] Minimal, pinned dependencies [x] No production/runtime code [x] Conventions configure only the applied project [x] Measured with build scans before it becomes a bottleneck ``` ## How to diagnose Use `./gradlew --scan` or `--profile` to see configuration time and whether buildSrc recompilation dominates. If a one-line change anywhere forces full re-configuration regularly, that's the signal buildSrc has grown past its comfort zone.

  • What is the signal that you've outgrown buildSrc?
    Frequent full re-configuration from small edits, slow buildSrc compilation dominating build scans, and a buildSrc that has become a large multi-concern plugin codebase — time to split into a build-logic composite build.
  • Why is reaching out with allprojects {} from a convention plugin a smell?
    It recreates cross-configuration coupling and serializes configuration, defeating project isolation; conventions should configure only the project they are applied to.

saying these in an interview costs you the question

  • Storing application/runtime code in buildSrc.
  • Letting buildSrc accumulate heavy, unpinned dependencies.
  • Defending a huge, constantly-edited buildSrc instead of recognizing the build-speed cost.

context