Learn the basics of structuring Gradle projects using subprojects and composite builds.

In this section, you will:

  • Understand Multi-Project builds

  • Understand Composite Builds

  • Add a subproject to your Build

  • Add a build to your Build

Step 0. Before you Begin

  1. You initialized your Java app in part 1.

  2. You understand the Gradle Build Lifecycle from part 2.

Step 1. About Multi-Project Builds

Typically, builds contain multiple projects, such as shared libraries or separate applications that will be deployed in your ecosystem.

In Gradle, a multi-project build consists of:

  • settings.gradle(.kts) file representing your Gradle build including required subprojects e.g. include("app", "model", "service")

  • build.gradle(.kts) and source code for each subproject in corresponding subdirectories

Our build currently consists of a root project called authoring-tutorial, which has a single app subproject:

.   (1)
├── app (2)
│   ... (3)
│   └── build.gradle.kts (4)
└── settings.gradle.kts  (5)
1 The authoring-tutorial root project
2 The app subproject
3 The app source code
4 The app build script
5 The optional settings file
.   (1)
├── app (2)
│   ... (3)
│   └── build.gradle (4)
└── settings.gradle  (5)
1 The authoring-tutorial root project
2 The app subproject
3 The app source code
4 The app build script
5 The optional settings file

Step 2. Add another Subproject to the Build

Imagine that our project is growing and requires a custom library to function.

Let’s create this imaginary lib. First, create a lib folder:

mkdir lib
cd lib

Create a file called build.gradle(.kts) and add the following lines to it:

lib/build.gradle.kts
plugins {
    id("java")
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.9.3")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
    implementation("com.google.guava:guava:32.1.1-jre")
}

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

tasks.register("task3"){
    println("REGISTER TASK3: This is executed during the configuration phase")
}

tasks.named("task3"){
    println("NAMED TASK3: This is executed during the configuration phase")
    doFirst {
        println("NAMED TASK3 - doFirst: This is executed during the execution phase")
    }
    doLast {
        println("NAMED TASK3 - doLast: This is executed during the execution phase")
    }
}
lib/build.gradle
plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.9.3'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    implementation 'com.google.guava:guava:32.1.1-jre'
}

test {
    useJUnitPlatform()
}

tasks.register('task3') {
    println('REGISTER TASK3: This is executed during the configuration phase')
}

tasks.named('task3') {
    println('NAMED TASK3: This is executed during the configuration phase')
    doFirst {
        println('NAMED TASK3 - doFirst: This is executed during the execution phase')
    }
    doLast {
        println('NAMED TASK3 - doLast: This is executed during the execution phase')
    }
}

Your project should look like this:

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

Let’s add some code to the lib subproject. Create a new directory:

mkdir -p lib/src/main/java/com/gradle

Create a Java class called CustomLib in a file called CustomLib.java with the following source code:

lib/src/main/java/com/gradle/CustomLib.java
package com.gradle;

public class CustomLib {
    public static String identifier = "I'm a String from a lib.";
}

The project should now have the following file and directory structure:

.
├── app
│   ├── build.gradle.kts
│   └── src
│       └── main
│           └── java
│               └── authoring
│                   └── tutorial
│                       └── App.java
├── lib
│   ├── build.gradle.kts
│   └── src
│       └── main
│           └── java
│               └── com
│                   └── gradle
│                       └── CustomLib.java
└── settings.gradle.kts
.
├── app
│   ├── build.gradle
│   └── src
│       └── main
│           └── java
│               └── authoring
│                   └── tutorial
│                       └── App.java
├── lib
│   ├── build.gradle
│   └── src
│       └── main
│           └── java
│               └── com
│                   └── gradle
│                       └── CustomLib.java
└── settings.gradle

However, the lib subproject does not belong to the build, and you won’t be able to execute task3, until it is added to the settings.gradle(.kts) file.

To add lib to the build, update the settings.gradle(.kts) file in the root accordingly:

settings.gradle.kts
plugins {
    id("org.gradle.toolchains.foojay-resolver-convention") version "0.7.0"
}

rootProject.name = "authoring-tutorial"

include("app")
include("lib") // Add lib to the build
settings.gradle
plugins {
    id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0'
}

rootProject.name = 'authoring-tutorial'

include('app')
include('lib') // Add lib to the build

Let’s add the lib subproject as an app dependency in app/build.gradle(.kts):

app/build.gradle.kts
dependencies {
    implementation(project(":lib")) // Add lib as an app dependency
}
app/build.gradle
dependencies {
    implementation(project(':lib')) // Add lib as an app dependency
}

Update the app source code so that it imports the lib:

app/src/main/java/authoring/tutorial/App.java
package authoring.tutorial;

import com.gradle.CustomLib;

public class App {
    public String getGreeting() {
        return "CustomLib identifier is: " + CustomLib.identifier;
    }

    public static void main(String[] args) {
        System.out.println(new App().getGreeting());
    }
}

Finally, let’s run the app with the command ./gradlew run:

$ ./gradlew run

> Configure project :app

> Task :app:processResources NO-SOURCE
> Task :lib:compileJava
> Task :lib:processResources NO-SOURCE
> Task :lib:classes
> Task :lib:jar
> Task :app:compileJava
> Task :app:classes

> Task :app:run
CustomLib identifier is: I'm a String from a lib.

BUILD SUCCESSFUL in 11s
8 actionable tasks: 6 executed, 2 up-to-date

Our build for the root project authoring-tutorial now includes two subprojects, app and lib. app depends on lib. You can build lib independent of app. However, to build app, Gradle will also build lib.

Step 3. Understand Composite Builds

A composite build is simply a build that includes other builds.

Composite builds allow you to:

  • Extract your build logic from your project build (and re-use it among subprojects)

  • Combine builds that are usually developed independently (such as a plugin and an application)

  • Decompose a large build into smaller, more isolated chunks

Step 4. Add build to the Build

Let’s add a plugin to our build. First, create a new directory called license-plugin in the gradle directory:

cd gradle
mkdir license-plugin
cd license-plugin

Once in the gradle/license-plugin directory, run gradle init. Make sure that you select the Gradle plugin project as well as the other options for the init task below:

$ gradle init --dsl kotlin --type kotlin-gradle-plugin --project-name license
$ gradle init --dsl groovy --type groovy-gradle-plugin --project-name license

Select defaults for any additional prompts.

Your project should look like this:

.
├── app
│   ...
│   └── build.gradle.kts
├── lib
│   ...
│   └── build.gradle.kts
├── gradle
│    ├── ...
│    └── license-plugin
│        ├── settings.gradle.kts
│        └── plugin
│            ├── gradle
│            │   └── ....
│            ├── src
│            │   ├── functionalTest
│            │   │   └── ....
│            │   ├── main
│            │   │   └── kotlin
│            │   │       └── license
│            │   │           └── LicensePlugin.kt
│            │   └── test
│            │       └── ...
│            └── build.gradle.kts
│
└── settings.gradle.kts
.
├── app
│   ...
│   └── build.gradle
├── lib
│   ...
│   └── build.gradle
├── gradle
│    ├── ...
│    └── license-plugin
│        ├── settings.gradle
│        └── plugin
│            ├── gradle
│            │   └── ....
│            ├── src
│            │   ├── functionalTest
│            │   │   └── ....
│            │   ├── main
│            │   │   └── groovy
│            │   │       └── license
│            │   │           └── LicensePlugin.groovy
│            │   └── test
│            │       └── ...
│            └── build.gradle
│
└── settings.gradle

Take the time to look at the LicensePlugin.kt or LicensePlugin.groovy code and the gradle/license-plugin/settings.gradle(.kts) file. It’s important to note that this is an entirely separate build with its own settings file and build script:

gradle/license-plugin/settings.gradle.kts
rootProject.name = "license"
include("plugin")
gradle/license-plugin/settings.gradle
rootProject.name = 'license'
include('plugin')

To add our license-plugin build to the root project, update the root settings.gradle(.kts) file accordingly:

settings.gradle.kts
plugins {
    id("org.gradle.toolchains.foojay-resolver-convention") version "0.7.0"
}

rootProject.name = "authoring-tutorial"

include("app")
include("subproject")

includeBuild("gradle/license-plugin") // Add the new build
settings.gradle
plugins {
    id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0'
}

rootProject.name = 'running-tutorial-groovy'

include('app')
include('lib')

includeBuild('gradle/license-plugin')

You can view the structure of the root project by running ./gradlew projects in the root folder authoring-tutorial:

$ ./gradlew projects

------------------------------------------------------------
Root project 'authoring-tutorial'
------------------------------------------------------------

Root project 'authoring-tutorial'
+--- Project ':app'
\--- Project ':lib'

Included builds
\--- Included build ':license-plugin'

Our build for the root project authoring-tutorial now includes two subprojects, app and lib, and another build, license-plugin.

When in the project root, running:

  • ./gradlew build - Builds app and lib

  • ./gradlew :app:build - Builds app and lib

  • ./gradlew :lib:build - Builds lib only

  • ./gradlew :license-plugin:plugin:build - Builds license-plugin only

There are many ways to design a project’s architecture with Gradle.

Multi-project builds are great for organizing projects with many modules such as mobile-app, web-app, api, lib, and documentation that have dependencies between them.

Composite (include) builds are great for separating build logic (i.e., convention plugins) or testing systems (i.e., patching a library)

Next Step: Settings File >>