skip to content

Walk through migrating shared build logic from buildSrc to an included build-logic build. What are the concrete steps and pitfalls?

level: seniorimportance: should knowfreq 30%

answer

  1. scaffold build-logic + own settings
  2. move convention plugin sources
  3. includeBuild in root settings
  4. switch consumers to plugins { id() }
  5. watch: catalog access, plugin-id match, lost global imports

basics

~10 s

Create build-logic/ with its own settings.gradle.kts, move convention plugin sources in, apply kotlin-dsl, add includeBuild('build-logic') to the root settings, then change subprojects to apply the plugins by id. Delete buildSrc once migrated.

solid answer

~40 s

Steps: (1) Create a `build-logic/` directory with its own `settings.gradle.kts` (`rootProject.name = "build-logic"`). (2) Add a plugin project applying `kotlin-dsl` (or `java-gradle-plugin`), and move your precompiled script plugins (e.g. `com.acme.java-conventions.gradle.kts`) and any custom tasks from `buildSrc/src/main/kotlin` into it. (3) In the **root** `settings.gradle.kts`, add `includeBuild("build-logic")`. (4) Change every consuming subproject from relying on the implicit classpath to applying the plugins **by id**: `plugins { id("com.acme.java-conventions") }`. (5) Delete `buildSrc`. Pitfalls: forgetting the included build's own settings file; expecting helper classes that were globally visible via buildSrc to still be importable (now they must be exposed through plugins or proper APIs); version-catalog access inside build-logic needs the catalog wired (typeUnsafe accessors or `versionCatalogs`); and plugin ids must match the precompiled script file names/package. Validate with `--configuration-cache` and a build scan.

code

kotlin · 13 lines
kotlin
// 1. build-logic/settings.gradle.kts
rootProject.name = "build-logic"

// 2. build-logic/build.gradle.kts
plugins { `kotlin-dsl` }

// 3. root settings.gradle.kts
includeBuild("build-logic")

// 4. app/build.gradle.kts (was relying on buildSrc classpath)
plugins { id("com.acme.java-conventions") }

// 5. remove buildSrc/ once green

go deeper

for a junior

List the happy-path steps: make build-logic, includeBuild it, apply by id, delete buildSrc.

for a middle

Add the precompiled-script-plugin id mechanics and the consumer plugins{} change.

for a senior

Call out the real pitfalls — catalog access, lost global visibility, id matching — and how to verify with config cache + build scans.

for a principal

Plan a staged rollout across many subprojects/repos, keep the diff reviewable, and decide whether to split build-logic into modules and later publish it.

## Goal Replace the implicit, global `buildSrc` with an explicit `includeBuild("build-logic")` composite so conventions become opt-in, isolated, testable, and potentially publishable — without changing what the conventions *do*. ## Step-by-step 1. **Scaffold build-logic.** ```kotlin // build-logic/settings.gradle.kts rootProject.name = "build-logic" ``` ```kotlin // build-logic/build.gradle.kts plugins { `kotlin-dsl` } ``` 2. **Move sources.** Relocate `buildSrc/src/main/kotlin/com.acme.java-conventions.gradle.kts` and custom task/extension classes into `build-logic/src/main/kotlin`. Precompiled script plugins keep their plugin id derived from the file name. 3. **Include it.** In the *root* `settings.gradle.kts`: ```kotlin includeBuild("build-logic") ``` 4. **Switch consumers to by-id application.** ```kotlin // app/build.gradle.kts plugins { id("com.acme.java-conventions") } ``` 5. **Delete buildSrc** once everything compiles and applies. ## Pitfalls - **Missing settings file.** An included build *must* have its own `settings.gradle.kts`; without it the build isn't recognized. - **Lost global visibility.** Helper classes that build scripts used directly (because buildSrc was on the classpath) are no longer auto-visible. Expose behavior through the convention plugins, custom tasks, or a published API — don't expect free imports. - **Version catalog access.** Inside build-logic, the root `libs` catalog isn't automatically available to precompiled script plugins. Common fixes: reference the generated catalog accessor via a dependency on the catalog, or read it through `extensions.getByType<VersionCatalogsExtension>()`. Plan this before moving dependency declarations. - **Plugin id mismatch.** The id consumers apply must match the precompiled script file name (including any package). A rename breaks `plugins { id(...) }`. - **gradle.properties / toolchain settings** that lived implicitly for buildSrc may need to be set in build-logic too. ## Verify Run the build with `--configuration-cache` and capture a build scan before and after. Confirm: conventions still apply, configuration cache reuses across runs, and editing one convention plugin reconfigures only its consumers. Keep the diff scoped — migrate, prove green, then optionally split build-logic into multiple plugin modules.

  • Why might a precompiled script plugin in build-logic fail to see the libs version catalog?
    The root catalog isn't auto-exposed to an included build's precompiled scripts; you must wire it — e.g. depend on the generated catalog accessor or read VersionCatalogsExtension — otherwise libs.* won't resolve.
  • What proves the migration didn't regress caching?
    Run with --configuration-cache before and after and compare build scans; confirm the cache is reused and that changing one convention plugin reconfigures only its consumers.
  • What commonly breaks when buildSrc is deleted?
    Build scripts that referenced buildSrc helper classes directly via the global classpath; those references must now go through plugins, tasks, or a published API.

saying these in an interview costs you the question

  • Forgetting build-logic needs its own settings.gradle.kts.
  • Assuming helper classes stay globally importable after dropping buildSrc.
  • Renaming the precompiled script file and breaking the plugin id consumers apply.

context