A multi-project build in Gradle consists of one root project and one or more subprojects.

structuring builds 1

Gradle can build the root project and any number of the subprojects in a single execution.

Project locations

Multi-project builds are represented by a tree with a single root. Each element in the tree represents a project.

Project and subproject are used interchangeably in this section.

A project has a path, which denotes the position of the project in the multi-project build tree.

In most cases, the project path is consistent with its location in the file system. However, this behavior is configurable if necessary.

The project tree is created in the settings.gradle(.kts) file. The location of the settings file is also the location of the root project.

A simple build

Let’s look at a basic multi-project build example that contains a root project and a single subproject.

The subproject is called app:

.
├── app
│   ...
│   └── build.gradle.kts
└── settings.gradle.kts
.
├── app
│   ...
│   └── build.gradle
└── settings.gradle

This is the recommended project structure for starting any Gradle project. The build init plugin also generates skeleton projects that follow this structure - a root project with a single subproject:

settings.gradle.kts
rootProject.name = "basic-multiproject"
include("app")
settings.gradle
rootProject.name = 'basic-multiproject'
include 'app'

In this case, Gradle will look for a build file in the app directory.

We can view the structure of a multi-project build by running the gradle projects command:

$ gradle -q projects

------------------------------------------------------------
Root project 'basic-multiproject'
------------------------------------------------------------

Root project 'basic-multiproject'
\--- Project ':app'

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

In the example below, the app subproject is a Java application that applies the application plugin and configures the main class accordingly:

app/build.gradle.kts
plugins {
    id("application")
}

application {
    mainClass = "com.example.Hello"
}
app/build.gradle
plugins {
    id 'application'
}

application {
    mainClass = 'com.example.Hello'
}
app/src/main/java/com/example/Hello.java
package com.example;

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, world!");
    }
}

We can then run the application by executing the run task from the application plugin.

$ gradle -q run
Hello, world!

Building the tree

In the settings file, you can use the include method to define the project tree:

settings.gradle.kts
include("project1", "project2:child1", "project3:child1")
settings.gradle
include 'project1', 'project2:child1', 'project3:child1'

The include method takes project paths as arguments. The project path is assumed to be equal to the relative physical file system path. For example, a path services:api is mapped by default to a folder ./services/api (relative to the project root .).

You only need to specify the leaves of the tree. This means that including the path services:hotels:api will create 3 projects: services, services:hotels, and services:hotels:api.

More examples of how to work with the project path can be found in the DSL documentation of Settings.include(java.lang.String[]).

Logical vs. Physical paths

You should avoid creating intermediate projects by changing the directory of included projects as needed:

include("/my/custom/path/subproject")

The physical and logical structure and location of projects (i.e, subprojects, modules) do not have to be identical.

A subproject located on disk at subs/web/my-web-module can have a logical name of :my-web-module or :subs:web:my-web-module depending on the settings.gradle(.kts) file:

include("my-web-module")        // :my-web-module
include("subs/my-web-module")   // :subs:web:my-web-module

Adding subprojects

Let’s add another subproject called lib to the previously created project.

All we need to do is add another include statement in the root settings file:

settings.gradle.kts
rootProject.name = "basic-multiproject"
include("app")
include("lib")
settings.gradle
rootProject.name = 'basic-multiproject'
include 'app'
include 'lib'

Gradle will then look for the build file of the new lib subproject in the ./lib/ subdirectory of the project:

.
├── app
│   ...
│   └── build.gradle.kts
├── lib
│   ...
│   └── build.gradle.kts
└── settings.gradle.kts
.
├── app
│   ...
│   └── build.gradle
├── lib
│   ...
│   └── build.gradle
└── settings.gradle

Using buildSrc for build logic

Complex build logic is a good candidate for being encapsulated as a custom task or binary plugin. Custom tasks and plugin implementations should not live in the build script.

buildSrc is a Gradle-recognized and protected directory for managing custom build logic and shared configuration among subprojects. It is ideal for custom plugins and custom tasks. It’s also great for keeping build scripts clean and implementation separate from declaration.

The buildSrc directory is treated as an included build. Upon discovering the directory, Gradle automatically compiles and tests this code and puts it in the classpath of your build script.

For multi-project builds, there can be only one buildSrc directory, which has to sit in the root project directory.

The downside of using buildSrc is that any change to it will cause every task in your project to be invalidated and have to rerun.

buildSrc uses the same source code conventions applicable to Java, Groovy, and Kotlin projects. It also provides direct access to the Gradle API.

Additional dependencies can be declared in a dedicated build.gradle(.kts) under buildSrc.

buildSrc/build.gradle.kts
repositories {
    mavenCentral()
}

dependencies {
    testImplementation("junit:junit:4.13")
}
buildSrc/build.gradle
repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'junit:junit:4.13'
}

A typical project including buildSrc has the following layout:

.
├── buildSrc
│  ├── build.gradle.kts
│  └── src
│      ├── main
│      │   └── java
│      │       └── com
│      │           └── enterprise
│      │               ├── Deploy.java
│      │               └── DeploymentPlugin.java
│      └── test
│          └── java
│              └── com
│                  └── enterprise
│                      └── DeploymentPluginTest.java
├── settings.gradle.kts
├── subproject-one
│   └── build.gradle.kts
└── subproject-two
    └── build.gradle.kts
.
├── buildSrc
│   ├── build.gradle
│   └── src
│       ├── main
│       │   └── java
│       │       └── com
│       │           └── enterprise
│       │               ├── Deploy.java
│       │               └── DeploymentPlugin.java
│       └── test
│           └── java
│               └── com
│                   └── enterprise
│                       └── DeploymentPluginTest.java
├── settings.gradle
├── subproject-one
│   └── build.gradle
└── subproject-two
    └── build.gradle

Adding buildSrc

Let’s add buildSrc to the previously created project and move common configuration to buildSrc/src/main/kotlin or buildSrc/src/main/groovy:

.
├── app
│   ...
│   └── build.gradle.kts
├── lib
│   ...
│   └── build.gradle.kts
├── buildSrc
│   ├── build.gradle.kts
│   └── src/main/kotlin/shared-build-configurations.gradle.kts
└── settings.gradle.kts
.
├── app
│   ...
│   └── build.gradle
├── lib
│   ...
│   └── build.gradle
├── buildSrc
│   ├── build.gradle
│   └── src/main/groovy/shared-build-configurations.gradle
└── settings.gradle

Gradle automatically compiles and tests the code in buildSrc and puts it in the classpath of your build script:

buildSrc/src/main/groovy/shared-build-configurations.gradle.kts
object Conventions {
    const val kotlinStdLib = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.21"
}

Which you can use accordingly:

app/build.gradle.kts
dependencies {
    implementation(Conventions.kotlinStdLib)
}

Modifying elements

The multi-project tree created in the settings file comprises project descriptors.

You can modify these descriptors in the settings file at any time.

To access a descriptor, you can:

settings.gradle.kts
include("project-a")
println(rootProject.name)
println(project(":project-a").name)
settings.gradle
include('project-a')
println rootProject.name
println project(':project-a').name

Using this descriptor, you can change the name, project directory, and build file of a project:

settings.gradle.kts
rootProject.name = "main"
include("project-a")
project(":project-a").projectDir = file("custom/my-project-a")
project(":project-a").buildFileName = "project-a.gradle.kts"
settings.gradle
rootProject.name = 'main'
include('project-a')
project(':project-a').projectDir = file('custom/my-project-a')
project(':project-a').buildFileName = 'project-a.gradle'

Consult the ProjectDescriptor class in the API documentation for more information.

Naming recommendations

As your project grows, naming and consistency get increasingly more important. To keep your builds maintainable, we recommend the following:

  1. Keep default project names for subprojects: It is possible to configure custom project names in the settings file. However, it’s an unnecessary extra effort for the developers to track which projects belong to what folders.

  2. Use lower case hyphenation for all project names: All letters are lowercase, and words are separated with a dash (-) character.

  3. Define the root project name in the settings file: The rootProject.name effectively assigns a name to the build, which is used in reports like build scans. If the root project name is not set, the name will be the container directory name, which can be unstable (i.e., you can check out your project in any directory). The name will be generated randomly if the root project name is not set and checked out to a file system’s root (e.g., / or C:\).