Sharing Build Logic using buildSrc
Keywords: buildsrc, build-logic, allprojects, subprojects

# Sharing Build Logic using `buildSrc`

version 9.1.0

Subprojects in a multi-project build often share common dependencies.

Rather than duplicating the same dependency declarations across multiple build scripts, Gradle allows you to centralize shared build logic in a special directory. This way, you can declare the dependency version in one place and have it automatically apply to all subprojects.

## Using `buildSrc`

`buildSrc` is a special directory in a Gradle build that allows you to organize and share build logic, such as custom plugins, tasks, configurations, and utility functions, across all projects in your build.

Let’s take a look at an example with the following structure:

A typical multi-project build has the following layout:

    .
    ├── api
    │   ├── src/
    │   └── build.gradle.kts    (1)
    ├── services
    │   ├── src/
    │   └── build.gradle.kts    (1)
    ├── shared
    │   ├── src/
    │   └── build.gradle.kts    (1)
    └── settings.gradle.kts

1 | A build script that contains build logic, including parts that are shared with other subprojects.

    .
    ├── api
    │   ├── src/
    │   └── build.gradle    (1)
    ├── services
    │   ├── src/
    │   └── build.gradle    (1)
    ├── shared
    │   ├── src/
    │   └── build.gradle    (1)
    └── settings.gradle

1 | A build script that contains build logic, including parts that are shared with other subprojects.

The build file for `api`, `services`, and `shared` has many commonalities:

api/build.gradle.kts

    plugins {
        `java-library`
    }

    repositories {
        mavenCentral()
    }

    dependencies {
        implementation("org.slf4j:slf4j-api:2.0.9")
        implementation("com.fasterxml.jackson.core:jackson-databind:2.17.1")
        testImplementation("org.junit.jupiter:junit-jupiter:5.11.3")
        testRuntimeOnly("org.junit.platform:junit-platform-launcher")
    }

    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(21)
        }
    }

    tasks.named<Test>("test") {
        useJUnitPlatform()
    }

services/build.gradle.kts

    plugins {
        `java-library`
    }

    repositories {
        mavenCentral()
    }

    dependencies {
        implementation("org.slf4j:slf4j-api:2.0.9")
        implementation("com.google.guava:guava:32.1.2-jre")
        testImplementation("org.junit.jupiter:junit-jupiter:5.11.3")
        testRuntimeOnly("org.junit.platform:junit-platform-launcher")
    }

    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(21)
        }
    }

    tasks.named<Test>("test") {
        useJUnitPlatform()
    }

shared/build.gradle.kts

    plugins {
        `java-library`
    }

    repositories {
        mavenCentral()
    }

    dependencies {
        implementation("org.slf4j:slf4j-api:2.0.9")
        implementation("com.google.guava:guava:32.1.2-jre")
        testImplementation("org.junit.jupiter:junit-jupiter:5.11.3")
        testRuntimeOnly("org.junit.platform:junit-platform-launcher")
    }

    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(21)
        }
    }

    tasks.named<Test>("test") {
        useJUnitPlatform()
    }

api/build.gradle

    plugins {
        id 'java-library'
    }

    repositories {
        mavenCentral()
    }

    dependencies {
        implementation 'org.slf4j:slf4j-api:2.0.9'
        implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
        testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3'
        testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    }

    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(21)
        }
    }

    tasks.named('test', Test) {
        useJUnitPlatform()
    }

services/build.gradle

    plugins {
        id 'java-library'
    }

    repositories {
        mavenCentral()
    }

    dependencies {
        implementation 'com.google.guava:guava:32.1.2-jre'
        implementation 'org.slf4j:slf4j-api:2.0.9'
        testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3'
        testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    }

    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(21)
        }
    }

    tasks.named('test', Test) {
        useJUnitPlatform()
    }

shared/build.gradle

    plugins {
        id 'java-library'
    }

    repositories {
        mavenCentral()
    }

    dependencies {
        implementation 'com.google.guava:guava:32.1.2-jre'
        implementation 'org.slf4j:slf4j-api:2.0.9'
        testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3'
        testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    }

    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(21)
        }
    }

    tasks.named('test', Test) {
        useJUnitPlatform()
    }

To avoid duplicating build logic across `api`, `services`, and `shared`, we can move the shared parts into `buildSrc`. This allows us to define dependencies and other configurations once and apply them uniformly to all subprojects.

For example, if you need to update the version of `org.slf4j:slf4j-api:1.7.32`, you only have to change it once—in the build logic inside `buildSrc`—rather than updating every individual build script.

Let’s expand the layout to include a `buildSrc` directory:

    .
    ├── buildSrc
    │   ├── src
    │   │   └──main
    │   │      └──kotlin
    │   │         └──java-common-conventions.gradle.kts  (1)
    │   └── build.gradle.kts
    ├── api
    │   ├── src/
    │   └── build.gradle.kts            (2)
    ├── services
    │   ├── src/
    │   └── build.gradle.kts            (2)
    ├── shared
    │   ├── src/
    │   └── build.gradle.kts            (2)
    └── settings.gradle.kts

1 | A shared build script.
2 | Applies the shared build script.

    .
    ├── buildSrc
    │   ├── src
    │   │   └──main
    │   │      └──groovy
    │   │         └──java-common-conventions.gradle  (1)
    │   └── build.gradle
    ├── api
    │   ├── src/
    │   └── build.gradle            (2)
    ├── services
    │   ├── src/
    │   └── build.gradle            (2)
    ├── shared
    │   ├── src/
    │   └── build.gradle            (2)
    └── settings.gradle

1 | A shared build script.
2 | Applies the shared build script.

When a `buildSrc` directory is present at the root of a Gradle build, Gradle treats it as a Composite Build. Upon detecting the `buildSrc` directory, Gradle:

- Treats `buildSrc` as an independent Gradle project with its own `build.gradle(.kts)` file and its own `src/` folder.

- Compiles all classes and scripts in `buildSrc` (typically under `src/main/kotlin` or `src/main/groovy`) before evaluating any other build scripts in the main build.

- Makes the compiled classes and scripts available on the classpath of all other project build scripts in the root project and subprojects.

As such, our new `buildSrc` has the following build file:

buildSrc/build.gradle.kts

    plugins {
        `kotlin-dsl`
    }

    repositories {
        gradlePluginPortal()
    }

buildSrc/build.gradle

    plugins {
        id 'groovy-gradle-plugin'
    }

    repositories {
        gradlePluginPortal()
    }

In the `buildSrc`, the build script `java-common-conventions.gradle(.kts)` is created in `src/main/kotlin` or `src/main/groovy`. It contains dependencies and other build information that is common to our subprojects:

buildSrc/src/main/kotlin/java-common-conventions.gradle.kts

    plugins {
        `java-library`
    }

    repositories {
        mavenCentral()
    }

    dependencies {
        implementation("org.slf4j:slf4j-api:2.0.9")
        testImplementation("org.junit.jupiter:junit-jupiter:5.11.3")
        testRuntimeOnly("org.junit.platform:junit-platform-launcher")
    }

    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(21)
        }
    }

    tasks.named<Test>("test") {
        useJUnitPlatform()
    }

buildSrc/src/main/groovy/java-common-conventions.gradle

    plugins {
        id 'java-library'
    }

    repositories {
        mavenCentral()
    }

    dependencies {
        implementation 'org.slf4j:slf4j-api:2.0.9'
        testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3'
        testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    }

    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(21)
        }
    }

    tasks.named('test', Test) {
        useJUnitPlatform()
    }

If you create a script file like `java-common-conventions.gradle(.kts)`, you can treat it as a plugin and apply it in your subprojects. The ID of the plugin is the name of the build file without the `gradle(.kts)` extension. This kind of plugin is called a convention plugin.

The shared logic is removed from the `api`, `services`, and `shared` build files. And the `shared` plugin is applied in the files instead:

api/build.gradle.kts

    plugins {
        id("java-common-conventions")
    }

    dependencies {
        implementation("com.fasterxml.jackson.core:jackson-databind:2.17.1")
    }

services/build.gradle.kts

    plugins {
        id("java-common-conventions")
    }

    dependencies {
        implementation("com.google.guava:guava:32.1.2-jre")
    }

shared/build.gradle.kts

    plugins {
        id("java-common-conventions")
    }

    dependencies {
        implementation("com.google.guava:guava:32.1.2-jre")
    }

api/build.gradle

    plugins {
        id("java-common-conventions")
    }

    dependencies {
        implementation("com.fasterxml.jackson.core:jackson-databind:2.17.1")
    }

services/build.gradle

    plugins {
        id("java-common-conventions")
    }

    dependencies {
        implementation("com.google.guava:guava:32.1.2-jre")
    }

shared/build.gradle

    plugins {
        id("java-common-conventions")
    }

    dependencies {
        implementation("com.google.guava:guava:32.1.2-jre")
    }

From now on, if you want to change something like the version of `slf4j-api`, you only need to update it in `buildSrc`, and the change will automatically apply to all subprojects.

## About the `buildSrc` directory

`buildSrc` is a special, Gradle-recognized directory that provides a convenient way to organize and reuse custom build logic across your build. Gradle automatically treats it as an included build with several advantages:

1. Reusable Build Logic: You can centralize common build logic, tasks, and plugins in `buildSrc`. This promotes consistency and maintainability across subprojects, as changes in `buildSrc` are automatically reflected wherever its logic is used.

2. Automatic Compilation and Classpath Inclusion: Gradle automatically compiles the code in `buildSrc` and includes it in the classpath of all build scripts. This allows classes, plugins, and utilities defined in `buildSrc` to be used directly without additional setup.

3. Cleaner Build Scripts: Moving logic into `buildSrc` keeps your project’s main build scripts focused and decluttered, improving readability and maintainability.

4. Ease of Testing: Since `buildSrc` is treated as a standalone build, you can write and run unit tests for your custom tasks and plugins just like any other project code.

5. Convenient Plugin Development: If you’re writing custom Gradle plugins for internal use, `buildSrc` provides an easy way to define and use them without publishing to an external repository.

`buildSrc` follows the same source layout conventions as regular Java, Groovy, or Kotlin projects and has direct access to the Gradle API. Dependencies can be declared in its own `build.gradle` or `build.gradle.kts` file.

In a multi-project build, only one `buildSrc` directory is allowed, and it must reside in the root project directory.

| Changes to code in `buildSrc` will invalidate the configuration phase and require re-execution of all tasks, potentially slowing down the build.

### Using a Composite Build Named `build-logic`

In addition to `buildSrc`, another powerful way to share build logic across subprojects is by using a dedicated composite build, typically named `build-logic`.

A typical multi-project build using `build-logic` as a composite build looks like this:

    .
    ├── build-logic/                   (1)
    │   ├── build.gradle.kts
    │   ├── settings.gradle.kts
    │   └── src/main/kotlin
    │       └── java-common-conventions.gradle.kts
    ├── api/
    │   └── build.gradle.kts           (2)
    ├── services/
    │   └── build.gradle.kts           (2)
    ├── shared/
    │   └── build.gradle.kts           (2)
    └── settings.gradle.kts            (3)

1 | Standalone build logic project that defines convention plugins.
2 | Applies shared plugin(s) from the composite build.
3 | Includes build-logic as an included build.

    .
    ├── build-logic/                (1)
    │   ├── build.gradle
    │   ├── settings.gradle
    │   └── src/main/groovy
    │       └── java-common-conventions.gradle
    ├── api/
    │   └── build.gradle            (2)
    ├── services/
    │   └── build.gradle            (2)
    ├── shared/
    │   └── build.gradle            (2)
    └── settings.gradle             (3)

1 | Standalone build logic project that defines convention plugins.
2 | Applies shared plugin(s) from the composite build.
3 | Includes `build-logic` as an included build.

You can learn more in Composite Builds.

## Avoid cross-project configuration using `subprojects` and `allprojects`

An improper way to share build logic between subprojects is cross-project configuration via the `subprojects {}` and `allprojects {}` DSL constructs.

With cross-project configuration, build logic can be injected into a subproject which is not obvious when looking at its build script.

In the long run, cross-project configuration usually grows in complexity and becomes a burden. Cross-project configuration can also introduce configuration-time coupling between projects, which can prevent optimizations like configuration-on-demand from working properly.

## Convention plugins versus cross-project configuration

The two most common uses of cross-project configuration can be better modeled using convention plugins:

1. Applying plugins or other configurations to subprojects of a certain type.
   Often, the cross-project configuration logic is `if subproject is of type X, then configure Y`. This is equivalent to applying `X-conventions` plugin directly to a subproject.

2. Extracting information from subprojects of a certain type.
   This use case can be modeled using outgoing configuration variants.