Subprojects in a multi-project build typically share some common traits.

structuring builds 3

For example, several subprojects may contain code in a particular programming language, while another subproject may be dedicated to documentation. Code quality rules apply to all the code subprojects but not the documentation subproject.

While the subprojects may share common traits, they serve different purposes. They produce different artifact types, for example:

  • public libraries - libraries that are published to some repository

  • internal libraries - libraries on which other subprojects depend

  • command line applications - applications with specific packaging requirements

  • web services - applications with specific packaging requirements

Additionally, some subprojects may be dedicated to testing.

The traits above identify a subproject’s type. In other words, a subproject’s type tells us what traits the subproject has.

Share logic using convention plugins

Gradle’s recommended way of organizing build logic is to use its plugin system.

A plugin should define the type of subproject.

In fact, Gradle core plugins are modeled the same way:

You can compose custom build logic by applying and configuring both core and external plugins. You can create custom plugins that define new project types and configure conventions specific to your project or organization.

For each example trait above, we can write a plugin that encapsulates the logic common to the subproject of a given type:

.
├── buildSrc
│   ├── src
│   │   └──main
│   │      └──kotlin
│   │         └──myproject.java-conventions.gradle      (1)
│   └── build.gradle.kts
├── api
│   ├── src
│   │   └──...
│   └── build.gradle.kts                                (2)
├── services
│   └── person-service
│       ├── src
│       │   └──...
│       └── build.gradle.kts                            (2)
├── shared
│   ├── src
│   │   └──...
│   └── build.gradle.kts                                (2)
└── settings.gradle.kts
1 Create the myproject.java-conventions convention plugin.
2 Applies the myproject.java-conventions convention plugin.
.
├── buildSrc
│   ├── src
│   │   └──main
│   │      └──kotlin
│   │         └──myproject.java-conventions.gradle.kts  (1)
│   └── build.gradle
├── api
│   ├── src
│   │   └──...
│   └── build.gradle                                    (2)
├── services
│   └── person-service
│       ├── src
│       │   └──...
│       └── build.gradle                                (2)
├── shared
│   ├── src
│   │   └──...
│   └── build.gradle                                    (2)
└── settings.gradle
1 Create the myproject.java-conventions convention plugin.
2 Applies the myproject.java-conventions convention plugin.

Share logic in buildSrc

We recommend putting source code and tests for the convention plugins in the buildSrc directory in the project’s root:

settings.gradle.kts
rootProject.name = "dependencies-java"
include("api", "shared", "services:person-service")
buildSrc/src/main/kotlin/myproject.java-conventions.gradle.kts
plugins {
    id("java")
}

group = "com.example"
version = "1.0"

repositories {
    mavenCentral()
}

dependencies {
    testImplementation("junit:junit:4.13")
}
api/build.gradle.kts
plugins {
    id("myproject.java-conventions")
}

dependencies {
    implementation(project(":shared"))
}
shared/build.gradle.kts
plugins {
    id("myproject.java-conventions")
}
services/person-service/build.gradle.kts
plugins {
    id("myproject.java-conventions")
}

dependencies {
    implementation(project(":shared"))
    implementation(project(":api"))
}
settings.gradle
rootProject.name = 'dependencies-java'
include 'api', 'shared', 'services:person-service'
buildSrc/src/main/groovy/myproject.java-conventions.gradle
plugins {
    id 'java'
}

group = 'com.example'
version = '1.0'

repositories {
    mavenCentral()
}

dependencies {
    testImplementation "junit:junit:4.13"
}
api/build.gradle
plugins {
    id 'myproject.java-conventions'
}

dependencies {
    implementation project(':shared')
}
shared/build.gradle
plugins {
    id 'myproject.java-conventions'
}
services/person-service/build.gradle
plugins {
    id 'myproject.java-conventions'
}

dependencies {
    implementation project(':shared')
    implementation project(':api')
}

Consult Using buildSrc for build logic to learn more.

Do not use cross-project configuration

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

Avoid using subprojects {} and allprojects {}.

With cross-configuration, build logic can be injected into a subproject, and this is not obvious when looking at the subproject’s build script, making it harder to understand the logic of a particular subproject. In the long run, cross-configuration usually grows in complexity and becomes a burden. Cross configuration can also introduce configuration-time coupling between projects, which can prevent optimizations like configuration-on-demand from working properly.

Convention plugins versus cross-configuration

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

  1. Applying plugins or other configuration to subprojects of a certain type.
    Often, the cross-configuration section will do 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.