# Best Practices for Structuring Builds

version 9.7.0-20260629004031+0000

## Modularize Your Builds

Modularize your builds by splitting your code into multiple projects.

### Explanation

Splitting your build’s source into multiple Gradle projects (modules) is essential for leveraging Gradle’s automatic work avoidance and parallelization features. When a source file changes, Gradle only recompiles the affected projects. If all your sources reside in a single project, Gradle can’t avoid recompilation and won’t be able to run tasks in parallel. Splitting your source into multiple projects can provide additional performance benefits by minimizing each subproject’s compilation classpath and ensuring code generating tools such as annotation and symbol processors run only on the relevant files.

Do this soon. Don’t wait until you hit some arbitrary number of source files or classes to do this, instead structure your build into multiple projects from the start using whatever natural boundaries exist in your codebase.

Exactly how to best split your source varies with every build, as it depends on the particulars of that build. Here are some common patterns we found that can work well and make cohesive projects:

- API vs. Implementation

- Front-end vs. Back-end

- Core business logic vs. UI

- Vertical slices (e.g., feature modules each containing UI + business logic)

- Inputs to source generation vs. their consumers

- Or simply closely related classes.

Ultimately, the specific scheme matters less than ensuring that your build is split logically and consistently.

Expanding a build to hundreds of projects is common, and Gradle is designed to scale to this size and beyond. In the extreme, tiny projects containing only a class or two are probably counterproductive. However, you should typically err on the side of adding more projects rather than fewer.

### Example

#### Don’t Do This

A common way to structure new builds

    ├── app // This project contains a mix of classes
    │    ├── build.gradle.kts
    │    └── src
    │        └── main
    │            └── java
    │                └── org
    │                    └── example
    │                        └── CommonsUtil.java
    │                        └── GuavaUtil.java
    │                        └── Main.java
    │                        └── Util.java
    ├── settings.gradle.kts

A common way to structure new builds

    ├── app // This project contains a mix of classes
    │    ├── build.gradle
    │    └── src
    │        └── main
    │            └── java
    │                └── org
    │                    └── example
    │                        └── CommonsUtil.java
    │                        └── GuavaUtil.java
    │                        └── Main.java
    │                        └── Util.java
    ├── settings.gradle

settings.gradle.kts

    include("app") (1)

settings.gradle

    include("app") (1)

build.gradle.kts

    plugins {
        application (2)
    }

    dependencies {
        implementation("com.google.guava:guava:31.1-jre") (3)
        implementation("commons-lang:commons-lang:2.6")
    }

    application {
        mainClass = "org.example.Main"
    }

build.gradle

    plugins {
        id 'application' (2)
    }

    dependencies {
        implementation 'com.google.guava:guava:31.1-jre' (3)
        implementation 'commons-lang:commons-lang:2.6'
    }

    application {
        mainClass = "org.example.Main"
    }

1 | This build contains only a single project (in addition to the root project) that contains all the source code. If there is any change to any source file, Gradle will have to recompile and rebuild everything. While incremental compilation will help (especially in this simplified example) this is still less efficient then avoidance. Gradle also won’t be able to run any tasks in parallel, since all these tasks are in the same project, so this design won’t scale nicely.
2 | As there is only a single project in this build, the `application` plugin must be applied here. This means that the `application` plugin will affect all source files in the build, even those which have no need for it.
3 | Likewise, the dependencies here are only needed by each particular implmentation of util. There’s no need for the implementation using Guava to have access to the Commons library, but it does because they are all in the same project. This also means that the classpath for each subproject is much larger than it needs to be, which can lead to longer build times and other confusion.

#### Do This Instead

A better way to structure this build

    ├── app
    │    ├── build.gradle.kts
    │    └── src
    │        └── main
    │            └── java
    │                └── org
    │                    └── example
    │                        └── Main.java
    ├── settings.gradle.kts
    ├── util
    │    ├── build.gradle.kts
    │    └── src
    │        └── main
    │            └── java
    │                └── org
    │                    └── example
    │                        └── Util.java
    ├── util-commons
    │    ├── build.gradle.kts
    │    └── src
    │        └── main
    │            └── java
    │                └── org
    │                    └── example
    │                        └── CommonsUtil.java
    └── util-guava
        ├── build.gradle.kts
        └── src
            └── main
                └── java
                    └── org
                        └── example
                            └── GuavaUtil.java

A better way to structure this build

    ├── app // App contains only the core application logic
    │    ├── build.gradle
    │    └── src
    │        └── main
    │            └── java
    │                └── org
    │                    └── example
    │                        └── Main.java
    ├── settings.gradle
    ├── util // Util contains only the core utility logic
    │    ├── build.gradle
    │    └── src
    │        └── main
    │            └── java
    │                └── org
    │                    └── example
    │                        └── Util.java
    ├── util-commons // One particular implementation of util, using Apache Commons
    │    ├── build.gradle
    │    └── src
    │        └── main
    │            └── java
    │                └── org
    │                    └── example
    │                        └── CommonsUtil.java
    └── util-guava // Another implementation of util, using Guava
        ├── build.gradle
        └── src
            └── main
                └── java
                    └── org
                        └── example
                            └── GuavaUtil.java

settings.gradle.kts

    include("app") (1)
    include("util")
    include("util-commons")
    include("util-guava")

settings.gradle

    include("app") (1)
    include("util")
    include("util-commons")
    include("util-guava")

build.gradle.kts

    // This is the build.gradle file for the app module

    plugins {
        application (2)
    }

    dependencies { (3)
        implementation(project(":util-guava"))
        implementation(project(":util-commons"))
    }

    application {
        mainClass = "org.example.Main"
    }

build.gradle

    // This is the build.gradle file for the app module

    plugins {
        id "application" (2)
    }

    dependencies { (3)
        implementation project(":util-guava")
        implementation project(":util-commons")
    }

    application {
        mainClass = "org.example.Main"
    }

build.gradle.kts

    // This is the build.gradle file for the util-commons module

    plugins { (4)
        `java-library`
    }

    dependencies { (5)
        api(project(":util"))
        implementation("commons-lang:commons-lang:2.6")
    }

build.gradle

    // This is the build.gradle file for the util-commons module

    plugins { (4)
        id "java-library"
    }

    dependencies { (5)
        api project(":util")
        implementation "commons-lang:commons-lang:2.6"
    }

build.gradle.kts

    // This is the build.gradle file for the util-guava module

    plugins {
        `java-library`
    }

    dependencies {
        api(project(":util"))
        implementation("com.google.guava:guava:31.1-jre")
    }

build.gradle

    // This is the build.gradle file for the util-guava module

    plugins {
        id "java-library"
    }

    dependencies {
        api project(":util")
        implementation "com.google.guava:guava:31.1-jre"
    }

1 | This build logically splits the source into multiple projects. Each project can be built independently, and Gradle can run tasks in parallel. This means that if you change a single source file in one of the projects, Gradle will only need to recompile and rebuild that project, not the entire build.
2 | The `application` plugin is only applied to the `app` project, which is the only project that needs it.
3 | Each project only adds the dependencies it needs. This means that the classpath for each subproject is much smaller, which can lead to faster build times and less confusion.
4 | Each project only adds the specific plugins it needs.
5 | Each project only adds the dependencies it needs. Projects can effectively use API vs. Implementation separation.

### References

- Structuring Projects with Gradle

- Organizing Gradle Projects

### Tags

`#structuring-builds`

## Do Not Put Source Files in the Root Project

Do not put source files in your root project; instead, put them in a separate project.

### Explanation

The root project is a special Project in Gradle that serves as the entry point for your build.

It is the place to configure some settings and conventions that apply globally to the entire build, that are not configured via Settings. For example, you can declare (but not apply) plugins here to ensure the same plugin version is consistently available across all projects and define other configurations shared by all projects within the build.

| Be careful not to apply plugins unnecessarily in the root project - many plugins only affect source code and should only be applied to the projects that contain source code.

The root project should not be used for source files, instead they should be located in a separate Gradle project.

Setting up your build like this from the start will also make it easier to add new projects as your build grows in the future.

### Example

#### Don’t Do This

A common way to structure new builds

    ├── build.gradle.kts // Applies the `java-library` plugin to the root project
    ├── settings.gradle.kts
    └── src // This directory shouldn't exist
        └── main
            └── java
                └── org
                    └── example
                        └── MyClass1.java

A common way to structure new builds

    ├── build.gradle // Applies the `java-library` plugin to the root project
    ├── settings.gradle
    └── src // This directory shouldn't exist
        └── main
            └── java
                └── org
                    └── example
                        └── MyClass1.java

build.gradle.kts

    plugins { (1)
        `java-library`
    }

build.gradle

    plugins {
        id 'java-library' (1)
    }

1 | The `java-library` plugin is applied to the root project, as there are Java source files are in the root project.

#### Do This Instead

A better way to structure new builds

    ├── core
    │    ├── build.gradle.kts // Applies the `java-library` plugin to only the `core` project
    │    └── src // Source lives in a "core" (sub)project
    │        └── main
    │            └── java
    │                └── org
    │                    └── example
    │                        └── MyClass1.java
    └── settings.gradle.kts

A better way to structure new builds

    ├── core
    │    ├── build.gradle // Applies the `java-library` plugin to only the `core` project
    │    └── src // Source lives in a "core" (sub)project
    │        └── main
    │            └── java
    │                └── org
    │                    └── example
    │                        └── MyClass1.java
    └── settings.gradle

settings.gradle.kts

    include("core") (1)

settings.gradle

    include("core") (1)

build.gradle.kts

    // This is the build.gradle.kts file for the core module

    plugins { (2)
        `java-library`
    }

build.gradle

    // This is the build.gradle file for the core module

    plugins { (2)
        id 'java-library'
    }

1 | The root project exists only to configure the build, informing Gradle of a (sub)project named `core`.
2 | The `java-library` plugin is only applied to the `core` project, which contains the Java source files.

### References

- Structuring Projects with Gradle

- Organizing Gradle Projects

### Tags

`#structuring-builds`

## Favor `build-logic` Composite Builds for Build Logic

You should set up a Composite Build (often called an "included build") to hold your build logic—including any custom plugins, convention plugins, and other build-specific customizations.

### Explanation

The preferred location for build logic is an included build (typically named `build-logic`), not in `buildSrc`.

The automatically available `buildSrc` is great for rapid prototyping, but it comes with some subtle disadvantages:

- There are classloader differences in how these 2 approaches behave that can be surprising; included builds are treated just like external dependencies, which is a simpler mental model. Dependency resolution behaves subtly differently in `buildSrc`.

- There can potentially be fewer task invalidations in a build when files in an included build are modified, leading to faster builds. Any change in `buildSrc` causes the entire build to become out-of-date, whereas changes in a subproject of an included build only cause projects in the build using the products of that particular subproject to be out-of-date.

- Included builds are complete Gradle builds and can be opened, worked on, and built independently as standalone projects. It is straightforward to publish their products, including plugins, in order to share them with other projects.

- The `buildSrc` project automatically applies the `java` plugin, which may be unnecessary.

One important caveat to this recommendation is when creating `Settings` plugins. Defining these in a `build-logic` project requires it to be included in the `pluginManagement` block of the main build’s `settings.gradle(.kts)` file, in order to make these plugins available to the build early enough to be applied to the `Settings` instance. This is possible, but reduces Build Caching capability, potentially impacting performance. A better solution is to use a separate, minimal, included build (e.g. `build-logic-settings`) to hold only `Settings` plugins.

Another potential reason to use `buildSrc` is if you have a very large number of subprojects within your included `build-logic`. Applying a different set of `build-logic` plugins to the subprojects in your including build will result in a different classpath being used for each. This may have performance implications and make your build harder to understand. Using different plugin combinations can cause features like Build Services to break in difficult to diagnose ways.

Ideally, there would be no difference between using `buildSrc` and an included build, as `buildSrc` is intended to behave like an implicitly available included build. However, due to historical reasons, these subtle differences still exist. As this changes, this recommendation may be revised in the future. For now, these differences can introduce confusion.

Since setting up a composite build requires only minimal additional configuration, we recommend using it over `buildSrc` in most cases, especially for creating convention plugins.

### Example

#### Don’t Do This

    ├── build.gradle.kts
    ├── buildSrc
    │    ├── build.gradle.kts
    │    └── src
    │        └── main
    │            └── java
    │                └── org
    │                    └── example
    │                        ├── MyPlugin.java
    │                        └── MyTask.java
    └── settings.gradle.kts

    ├── build.gradle
    ├── buildSrc
    │    ├── build.gradle
    │    └── src
    │        └── main
    │            └── java
    │                └── org
    │                    └── example
    │                        ├── MyPlugin.java
    │                        └── MyTask.java
    └── settings.gradle

build.gradle.kts

    // This file is located in /buildSrc

    plugins {
        `java-gradle-plugin`
    }

    gradlePlugin {
        plugins {
            create("myPlugin") {
                id = "org.example.myplugin"
                implementationClass = "org.example.MyPlugin"
            }
        }
    }

build.gradle

    // This file is located in /buildSrc

    plugins {
        id "java-gradle-plugin"
    }

    gradlePlugin {
        plugins {
            create("myPlugin") {
                id = "org.example.myplugin"
                implementationClass = "org.example.MyPlugin"
            }
        }
    }

Set up a Plugin Build: This is the same using either method.

settings.gradle.kts

    rootProject.name = "favor-composite-builds"

settings.gradle

    rootProject.name = "favor-composite-builds"

`buildSrc` products are automatically usable: There is no additional configuration with this method.

#### Do This Instead

    ├── build-logic
    │    ├── plugin
    │    │    ├── build.gradle.kts
    │    │    └── src
    │    │        └── main
    │    │            └── java
    │    │                └── org
    │    │                    └── example
    │    │                        ├── MyPlugin.java
    │    │                        └── MyTask.java
    │    └── settings.gradle.kts
    ├── build.gradle.kts
    └── settings.gradle.kts

    ├── build-logic
    │    ├── plugin
    │    │    ├── build.gradle
    │    │    └── src
    │    │        └── main
    │    │            └── java
    │    │                └── org
    │    │                    └── example
    │    │                        ├── MyPlugin.java
    │    │                        └── MyTask.java
    │    └── settings.gradle
    ├── build.gradle
    └── settings.gradle

build.gradle.kts

    // This file is located in /build-logic/plugin

    plugins {
        `java-gradle-plugin`
    }

    gradlePlugin {
        plugins {
            create("myPlugin") {
                id = "org.example.myplugin"
                implementationClass = "org.example.MyPlugin"
            }
        }
    }

build.gradle

    // This file is located in /build-logic/plugin

    plugins {
        id "java-gradle-plugin"
    }

    gradlePlugin {
        plugins {
            create("myPlugin") {
                id = "org.example.myplugin"
                implementationClass = "org.example.MyPlugin"
            }
        }
    }

Set up a Plugin Build: This is the same using either method.

settings.gradle.kts

    // This file is located in the root project

    includeBuild("build-logic") (1)

    rootProject.name = "favor-composite-builds"

settings.gradle

    // This file is located in the root project

    includeBuild("build-logic") (1)

    rootProject.name = "favor-composite-builds"

settings.gradle.kts

    // This file is located in /build-logic

    rootProject.name = "build-logic"

    include("plugin") (2)

settings.gradle

    // This file is located in /build-logic

    rootProject.name = "build-logic"

    include("plugin") (2)

1 | Composite builds must be explicitly included: Use the `includeBuild` method to locate and include a build in order to use its products.
2 | Structure your included build into subprojects: This allows the main build to only depend on the necessary parts of the included build.

### References

- Composite Builds in the Multi-Project Builds Tutorial

- Composite Builds reference documentation

- Gradle Issue #6045: buildSrc vs. included builds

### Tags

`#structuring-builds`, `#composite-builds`

## Avoid Unintentionally Creating Empty Projects

When using a hierarchical directory structure to organize your Gradle projects, make sure to avoid unintentionally creating empty projects in your build.

### Explanation

When you use the Settings.include() method to include a project in your Grade settings file, you typically include projects by supplying the directory name like `include("featureA")`. This usage assumes that `featureA` is located at the root of your build.

You can include projects located in nested subdirectories by specifying their full project path using `:` as a separator between path segments. For instance, if project `search` was located in a subdirectory named `features`, itself located in a subdirectory named `subs`, you could call `include(":subs:features:search")` to include it.

Nesting projects in a sensible hierarchical directory structure is common practice in larger Gradle builds. This approach helps organize large builds and improves comprehensibility, compared to placing all projects directly under the build’s root.

However, without further configuration, Gradle will create empty projects for each element in every hierarchical path, even if some of those directories do not contain actual Gradle projects. In the example above, Gradle will create a project named `:subs`, a project named `:subs:features`, and a project named `:subs:features:search`. This behavior is usually not intended, as you likely only want to include the `search` project.

Unused projects - even if empty - can surprise maintainers, clutter reports, and make your build harder to understand. They also introduce unintended side effects. If you use `allprojects { …​ }` or `subprojects { …​ }`, plugins and configuration blocks will apply to every project, including the empty ones. This can degrade build performance. Additionally, invoking tasks on deeply nested projects requires using the full project path, such as `gradle :subs:features:search:build`, instead of the shorter `gradle :search:build`.

To avoid these downsides when using a hierarchical project structure, you can provide a flat name when including the project and explicitly set the Project.projectDir property for any projects located in nested directories:

    include(':my-web-module')
    project(':my-web-module').projectDir = file("subs/web/my-web-module")

This will prevent Gradle from creating empty projects for each element of the project’s path.

| Always use an identical logical project name and physical project location to avoid confusion. Don’t include a project named `:search` and locate it at `features/ui/default-search-toolbar`, as this will lead to confusion about the location of the project. Instead, locate this project at `features/ui/search`.

You should avoid unnecessarily deep directory structures. For builds containing only a few projects, it’s usually better to keep the structure flat by placing all projects at the root of the build. This eliminates the need to explicitly set `projectDir`. Within the context of a particular build, the pathless project name should clearly indicate where the project is located. You can also run the projects report for more information about the projects in your build and their locations.

If you find yourself facing ambiguity about project locations, consider simplifying the directory layout by flattening the structure, or using longer, more descriptive project names.

### Example

#### Don’t Do This

    ├── settings.gradle.kts
    ├── app/ (1)
    │   ├── build.gradle.kts
    │   └── src/
    └── subs/ (2)
        └── web/ (3)
            ├── my-web-module/ (4)
                ├── src/
                └── build.gradle.kts

    ├── settings.gradle
    ├── app/ (1)
    │   ├── build.gradle
    │   └── src/
    └── subs/ (2)
        └── web/ (3)
            ├── my-web-module/ (4)
                ├── src/
                └── build.gradle

1 | A project named `app` located at the root of the build
2 | A directory named `subs` that is not intended to represent a Gradle project, but is used to organize the build
3 | Another organizational directory not intended to represent a Gradle project
4 | A Gradle project named `my-web-module` that should be included in the build

settings.gradle.kts

    include(":app") (1)
    include(":subs:web:my-web-module") (2)

settings.gradle

    include(":app") (1)
    include(":subs:web:my-web-module") (2)

1 | Including the `app` project located at the root of the build requires no additional configuration
2 | Including a project named `:subs:my-web-module` located in a nested subdirectory causes Gradle to create empty projects for each element of the path

avoidEmptyProjects-avoid.out

    > Task :projects

    Projects:

    ------------------------------------------------------------
    Root project 'avoidEmptyProjects-avoid'
    ------------------------------------------------------------

    Location: /home/user/gradle/samples

    Project hierarchy:

    Root project 'avoidEmptyProjects-avoid'
    +--- Project ':app'
    \--- Project ':subs'
         \--- Project ':subs:web'
              \--- Project ':subs:web:my-web-module'

    Project locations:

    project ':app' - /app
    project ':subs' - /subs
    project ':subs:web' - /subs/web
    project ':subs:web:my-web-module' - /subs/web/my-web-module

    To see a list of the tasks of a project, run gradle <project-path>:tasks
    For example, try running gradle :app:tasks

    BUILD SUCCESSFUL in 0s
    1 actionable task: 1 executed

The output of running the `projects` report on the above build shows that Gradle created empty projects for `:subs` and `:subs:web`.

#### Do This Instead

settings.gradle.kts

    include(":app")

    include(":my-web-module")
    project(":my-web-module").projectDir = file("subs/web/my-web-module") (1)

settings.gradle

    include(":app")

    include(":my-web-module")
    project(":my-web-module").projectDir = file("subs/web/my-web-module") (1)

1 | After including the `:subs:web:my-web-module` project, its `projectDir` property is set to the physical location of the project

avoidEmptyProjects-do.out

    > Task :projects

    Projects:

    ------------------------------------------------------------
    Root project 'avoidEmptyProjects-do'
    ------------------------------------------------------------

    Location: /home/user/gradle/samples

    Project hierarchy:

    Root project 'avoidEmptyProjects-do'
    +--- Project ':app'
    \--- Project ':my-web-module'

    Project locations:

    project ':app' - /app
    project ':my-web-module' - /subs/web/my-web-module

    To see a list of the tasks of a project, run gradle <project-path>:tasks
    For example, try running gradle :app:tasks

    BUILD SUCCESSFUL in 0s
    1 actionable task: 1 executed

The output of running the `projects` report on the above build shows that now Gradle only creates the intended projects for this build.

You can also now invoke tasks on the `my-web-module` project using the shorter name `:my-web-module` like `gradle :my-web-module:build`, instead of `gradle :subs:web:my-web-module:build`.

### References

- Multi-Project Builds

### Tags

`#structuring-builds`

## Use Convention Plugins for Common Build Logic

Use convention plugins to encapsulate and reuse shared build logic across multiple projects in your build.

### Explanation

Instead of duplicating configuration across multiple build scripts, you can easily move common logic into a reusable convention plugins.

This approach offers several benefits:

- Reduces duplication: Shared build logic lives in one place, making your build easier to understand.

- Unlocks modularization: Convention plugins can apply other convention plugins, allowing you to orchestrate your build logic from small pieces.

- Centralizes configuration: Updates to build behavior can be made in one file instead of many.

- Keeps build files clean: Project build files stay focused on project-specific configuration.

- Improves IDE support: IDEs can better understand and validate build logic when it is structured in plugins.

Convention plugins are quicker to create than typed binary plugins extending the `Plugin` class. They are often a better choice for build logic that does not need to be shared outside a build, and that is simple enough to not require additional type safeness and testability benefits. Unlike binary plugins, convention plugins allow accessing plugin extensions, tasks and configurations via static accessors in build scripts written in Kotlin.

While setting up convention plugins takes some initial effort, it pays off by simplifying maintenance, improving comprehensibility, and making it easier to add new projects as your codebase grows.

As mentioned in Favor `build-logic` Composite Builds for Build Logic, we recommend placing your convention plugins in an included build (often named `build-logic`) instead of `buildSrc`.

### Example

#### Don’t Do This

project-a/build.gradle.kts

    plugins {
        `java-library`
    }

    // Duplicated configuration across multiple build files
    java {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    tasks.withType<JavaCompile>().configureEach {
        options.encoding = "UTF-8"
        options.compilerArgs.addAll(listOf("-Xlint:unchecked", "-Xlint:deprecation")) (1)
    }

    tasks.test {
        useJUnitPlatform()
        maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).takeIf { it > 0 } ?: 1 (2)
    }

    dependencies {
        testImplementation("org.junit.jupiter:junit-jupiter:5.9.3") (3)
    }

project-b/build.gradle.kts

    plugins {
        `java-library`
    }

    // Duplicated configuration across multiple build files
    java {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    tasks.withType<JavaCompile>().configureEach {
        options.encoding = "UTF-8"
        options.compilerArgs.addAll(listOf("-Xlint:unchecked", "-Xlint:deprecation")) (1)
    }

    tasks.test {
        useJUnitPlatform()
        maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).takeIf { it > 0 } ?: 1 (2)
    }

    dependencies {
        testImplementation("org.junit.jupiter:junit-jupiter:5.9.3") (3)
        api("com.google.guava:guava:23.0") (4)
    }

project-a/build.gradle

    plugins {
        id("java-library")
    }

    // Duplicated configuration across multiple build files
    java {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    tasks.withType(JavaCompile).configureEach {
        options.encoding = "UTF-8"
        options.compilerArgs += ["-Xlint:unchecked", "-Xlint:deprecation"] (1)
    }

    test {
        useJUnitPlatform()
        maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 (2)
    }

    dependencies {
        testImplementation("org.junit.jupiter:junit-jupiter:5.9.3") (3)
    }

project-b/build.gradle

    plugins {
        id("java-library")
    }

    // Duplicated configuration across multiple build files
    java {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    tasks.withType(JavaCompile).configureEach {
        options.encoding = "UTF-8"
        options.compilerArgs += ["-Xlint:unchecked", "-Xlint:deprecation"] (1)
    }

    test {
        useJUnitPlatform()
        maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 (2)
    }

    dependencies {
        testImplementation("org.junit.jupiter:junit-jupiter:5.9.3") (3)
        api("com.google.guava:guava:23.0") (4)
    }

1 | Common compiler settings repeated across multiple projects.
2 | Shared test configuration that must be maintained in multiple places.
3 | Common dependencies that could be managed centrally.
4 | Unique project dependencies.

#### Do This Instead

Create included build containing convention plugins for your build in `build-logic` and add it to your settings file:

settings.gradle.kts

    pluginManagement {
        includeBuild("build-logic") (1)
    }

build-logic/build.gradle.kts

    plugins {
        `kotlin-dsl` (2)
    }

settings.gradle

    pluginManagement {
        includeBuild("build-logic") (1)
    }

build-logic/build.gradle

    plugins {
        id("groovy-gradle-plugin") (2)
    }

1 | Include the `build-logic` build, which defines convention plugins.
2 | Enable the use of Kotlin DSL in `build-logic`.

Create convention plugins for each type of project in `build-logic`:

build-logic/src/main/kotlin/my.base-java-library.gradle.kts

    plugins {
        `java-library`
    }

    java { (1)
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    tasks.withType<JavaCompile>().configureEach {
        options.encoding = "UTF-8"
        options.compilerArgs.addAll(listOf("-Xlint:unchecked", "-Xlint:deprecation"))
    }

build-logic/src/main/kotlin/my.java-library.gradle.kts

    plugins { (2)
        id("my.base-java-library")
        id("my.java-use-junit5")
    }

build-logic/src/main/kotlin/my.java-use-junit5.gradle.kts

    plugins {
        `java-library`
    }

    tasks.withType<Test>().configureEach { (3)
        useJUnitPlatform()
        maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).takeIf { it > 0 } ?: 1
    }

    dependencies {
        testImplementation("org.junit.jupiter:junit-jupiter:5.9.3")
    }

build-logic/src/main/groovy/my.base-java-library.gradle

    plugins {
        id("java-library")
    }

    java { (1)
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    tasks.withType(JavaCompile).configureEach {
        options.encoding = "UTF-8"
        options.compilerArgs += ["-Xlint:unchecked", "-Xlint:deprecation"]
    }

build-logic/src/main/groovy/my.java-library.gradle

    plugins { (2)
        id("my.base-java-library")
        id("my.java-use-junit5")
    }

build-logic/src/main/groovy/my.java-use-junit5.gradle

    plugins {
        id("java-library")
    }

    tasks.withType(Test).configureEach { (3)
        useJUnitPlatform()
        maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
    }

    dependencies {
        testImplementation("org.junit.jupiter:junit-jupiter:5.9.3")
    }

1 | Default settings for a Java library plugin.
2 | A convention plugin can apply other convention plugins.
3 | JUnit 5 configuration moved to a convention plugin.

And apply these plugin in any build files to use the shared logic:

project-a/build.gradle.kts

    plugins {
        id("my.java-library") (6)
    }

project-b/build.gradle.kts

    plugins {
        id("my.java-library") (6)
    }

    dependencies {
        api("com.google.guava:guava:23.0") (7)
    }

project-a/build.gradle

    plugins {
        id("my.java-library") (6)
    }

project-b/build.gradle

    plugins {
        id("my.java-library") (6)
    }

    dependencies {
        api("com.google.guava:guava:23.0") (7)
    }

### References

- Developing Custom Gradle Plugins

- Implementing Pre-compiled Script Plugins

- Types of Plugins

### Tags

`#structuring-builds`