You can open this sample in an IDE that supports Gradle.

To prepare your software project for growth, you can organize a Gradle project into multiple subprojects to modularize the software you are building. In this guide, you’ll learn how to structure such a project on the example of a Java application. However, the general concepts apply for any software you are building with Gradle. You can follow the guide step-by-step to create a new project from scratch or download the complete sample project using the links above.

What you’ll build

You’ll build a Java application that consists of an application and multiple library projects.

What you’ll need

Create a project folder

Gradle comes with a built-in task, called init, that initializes a new Gradle project in an empty folder. The init task uses the (also built-in) wrapper task to create a Gradle wrapper script, gradlew.

The first step is to create a folder for the new project and change directory into it.

$ mkdir demo
$ cd demo

Run the init task

From inside the new project directory, run the init task using the following command in a terminal: gradle init. When prompted, select the 1: application project type and 1: Java as the implementation language. Afterwards, select 2: Application and library project. Next you can choose the DSL for writing buildscripts - 1 : Kotlin or 2: Groovy. For the other questions, press enter to use the default values.

The output will look like this:

$ gradle init

Select type of build to generate:
  1: Application
  2: Library
  3: Gradle plugin
  4: Basic (build structure only)
Enter selection (default: Application) [1..4] 1

Select implementation language:
  1: Java
  2: Kotlin
  3: Groovy
  4: Scala
  5: C++
  6: Swift
Enter selection (default: Java) [1..6]  1

Project name (default: demo):

Enter target Java version (min: 7, default: 21):

Select application structure:
  1: Single application project
  2: Application and library project
Enter selection (default: Single application project) [1..2] 2

Select build script DSL:
  1: Kotlin
  2: Groovy
Enter selection (default: Kotlin) [1..2]

Select test framework:
  1: JUnit 4
  2: TestNG
  3: Spock
  4: JUnit Jupiter
Enter selection (default: JUnit Jupiter) [1..4]

Generate build using new APIs and behavior (some features may change in the next minor release)? (default: no) [yes, no]

BUILD SUCCESSFUL
1 actionable task: 1 executed

The init task generates the new project with the following structure:

├── gradle (1)
│   ├── libs.versions.toml (2)
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew (3)
├── gradlew.bat (3)
├── settings.gradle.kts (4)
├── buildSrc
│   ├── build.gradle.kts (5)
│   ├── settings.gradle.kts (5)
│   └── src
│       └── main
│           └── kotlin (6)
│               ├── buildlogic.java-application-conventions.gradle.kts
│               ├── buildlogic.java-common-conventions.gradle.kts
│               └── buildlogic.java-library-conventions.gradle.kts
├── app
│   ├── build.gradle.kts (7)
│   └── src
│       ├── main (8)
│       │   └── java
│       │       └── demo
│       │           └── app
│       │               ├── App.java
│       │               └── MessageUtils.java
│       └── test (9)
│           └── java
│               └── demo
│                   └── app
│                       └── MessageUtilsTest.java
├── list
│   ├── build.gradle.kts (7)
│   └── src
│       ├── main (8)
│       │   └── java
│       │       └── demo
│       │           └── list
│       │               └── LinkedList.java
│       └── test (9)
│           └── java
│               └── demo
│                   └── list
│                       └── LinkedListTest.java
└── utilities
    ├── build.gradle.kts (7)
    └── src
        └── main (8)
            └── java
                └── demo
                    └── utilities
                        ├── JoinUtils.java
                        ├── SplitUtils.java
                        └── StringUtils.java
├── gradle (1)
│   ├── libs.versions.toml (2)
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew (3)
├── gradlew.bat (3)
├── settings.gradle (4)
├── buildSrc
│   ├── build.gradle (5)
│   ├── settings.gradle (5)
│   └── src
│       └── main
│           └── groovy (6)
│               ├── buildlogic.java-application-conventions.gradle
│               ├── buildlogic.java-common-conventions.gradle
│               └── buildlogic.java-library-conventions.gradle
├── app
│   ├── build.gradle (7)
│   └── src
│       ├── main (8)
│       │   └── java
│       │       └── demo
│       │           └── app
│       │               ├── App.java
│       │               └── MessageUtils.java
│       └── test (9)
│           └── java
│               └── demo
│                   └── app
│                       └── MessageUtilsTest.java
├── list
│   ├── build.gradle (7)
│   └── src
│       ├── main (8)
│       │   └── java
│       │       └── demo
│       │           └── list
│       │               └── LinkedList.java
│       └── test (9)
│           └── java
│               └── demo
│                   └── list
│                       └── LinkedListTest.java
└── utilities
    ├── build.gradle (7)
    └── src
        └── main (8)
            └── java
                └── demo
                    └── utilities
                        ├── JoinUtils.java
                        ├── SplitUtils.java
                        └── StringUtils.java
1 Generated folder for wrapper files
2 Generated version catalog
3 Gradle wrapper start scripts
4 Settings file to define build name and subprojects
5 Build script of buildSrc to configure dependencies of the build logic
6 Source folder for convention plugins written in Groovy or Kotlin DSL
7 Build script of the three subprojects - app, list and utilities
8 Java source folders in each of the subprojects
9 Java test source folders in the subprojects

You now have the project setup to build a Java application which is modularized into multiple subprojects.

Review the project files

The settings.gradle(.kts) file has two interesting lines:

settings.gradle.kts
rootProject.name = "demo"
include("app", "list", "utilities")
settings.gradle
rootProject.name = 'demo'
include('app', 'list', 'utilities')
  • rootProject.name assigns a name to the build, which overrides the default behavior of naming the build after the directory it’s in. It’s recommended to set a fixed name as the folder might change if the project is shared - e.g. as root of a Git repository.

  • include("app", "list", "utilities") defines that the build consists of three subprojects in the corresponding folders. More subprojects can be added by extending the list or adding more include(…​) statements.

Since our build consists of multiple-subprojects, we want to share build logic and configuration between them. For this, we utilize so-called convention plugins that are located in the buildSrc folder. Convention plugins in buildSrc are an easy way to utilise Gradle’s plugin system to write reusable bits of build configuration.

in this sample, we can find three such convention plugins that are based on each other:

buildSrc/src/main/kotlin/buildlogic.java-common-conventions.gradle.kts
plugins {
    java (1)
}

repositories {
    mavenCentral() (2)
}

dependencies {
    constraints {
        implementation("org.apache.commons:commons-text:1.12.0") (3)
    }

    testImplementation("org.junit.jupiter:junit-jupiter:5.10.3") (4)

    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.named<Test>("test") {
    useJUnitPlatform() (5)
}
buildSrc/src/main/groovy/buildlogic.java-common-conventions.gradle
plugins {
    id 'java' (1)
}

repositories {
    mavenCentral() (2)
}

dependencies {
    constraints {
        implementation 'org.apache.commons:commons-text:1.12.0' (3)
    }

    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.3' (4)

    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
    useJUnitPlatform() (5)
}

The java-common-conventions defines some configuration that should be shared by all our Java project — independent of whether they represent a library or the actual application. First, we apply the Java Plugin (1) to have all functionality for building Java projects available. Then, we declare a repository — mavenCentral() — as source for external dependencies (2), define dependency constraints (3) as well as standard dependencies that are shared by all subprojects and set JUnit 5 as testing framework (4…​). Other shared settings, like compiler flags or JVM version compatibilities, could also be set here.

buildSrc/src/main/kotlin/buildlogic.java-library-conventions.gradle.kts
plugins {
    id("buildlogic.java-common-conventions") (1)
    `java-library` (2)
}
buildSrc/src/main/groovy/buildlogic.java-library-conventions.gradle
plugins {
    id 'buildlogic.java-common-conventions' (1)
    id 'java-library' (2)
}
buildSrc/src/main/kotlin/buildlogic.java-application-conventions.gradle.kts
plugins {
    id("buildlogic.java-common-conventions") (1)
    application (2)
}
buildSrc/src/main/groovy/buildlogic.java-application-conventions.gradle
plugins {
    id 'buildlogic.java-common-conventions' (1)
    id 'application' (2)
}

Both java-library-conventions and java-application-conventions apply the java-common-conventions plugin (1) so that the configuration performed there is shared by library and application projects alike. Next they apply the java-library or application plugin respectively (2) thus combining our common configuration logic with specifics for a library or application. While there is no more fine grained configuration in this example, library or application project specific build configuration can go into one of these convention plugin scripts.

Lets have a look at the build.gradle(.kts) files in the subprojects.

app/build.gradle.kts
plugins {
    id("buildlogic.java-application-conventions")
}

dependencies {
    implementation("org.apache.commons:commons-text")
    implementation(project(":utilities"))
}

application {
    mainClass = "demo.app.App" (1)
}
app/build.gradle
plugins {
    id 'buildlogic.java-application-conventions'
}

dependencies {
    implementation 'org.apache.commons:commons-text'
    implementation project(':utilities')
}

application {
    mainClass = 'demo.app.App' (1)
}
list/build.gradle.kts
plugins {
    id("buildlogic.java-library-conventions")
}
list/build.gradle
plugins {
    id 'buildlogic.java-library-conventions'
}
utilities/build.gradle.kts
plugins {
    id("buildlogic.java-library-conventions")
}

dependencies {
    api(project(":list"))
}
utilities/build.gradle
plugins {
    id 'buildlogic.java-library-conventions'
}

dependencies {
    api project(':list')
}

Looking at the build scripts, we can see that they include up to three blocks

  • Every build script should have a plugins {} block to apply plugins. In a well-structured build, it may only apply one convention plugin as in this example. The convention plugin will then take care of applying and configuring core Gradle plugins (like application or java-library) other convention plugin or community plugins from the Plugin Portal.

  • Second, if the project has dependencies, a dependencies {} block should be added. Dependencies can be external, such as the JUnit dependencies we add in java-common-conventions, or can point to other local subprojects. For this, the project(…​) notation is used. In our example, the utilities library requires the list library. And the app makes use of the utilities library. If local projects depend on each other, Gradle takes care of building dependent projects if (and only if) needed. To learn more, have a look at the documentation about dependency management in Gradle.

  • Third, there may be one or multiple configuration blocks for plugins. These should only be used in build scripts directly if they configure something specific for the one project. Otherwise, such configurations also belong into a convention plugin. In this example, we use the application {} block, which is specific to the application plugin, to set the mainClass in our app project to demo.app.App (1).

The last build file we have is the build.gradle(.kts) file in buildSrc.

buildSrc/build.gradle.kts
plugins {
    `kotlin-dsl` (1)
}

repositories {
    gradlePluginPortal() (2)
}
buildSrc/build.gradle
plugins {
    id 'groovy-gradle-plugin' (1)
}

repositories {
    gradlePluginPortal() (2)
}

This file is setting the stage to build the convention plugins themselves. By applying one of the plugins for plugin development — groovy-gradle-plugin or kotlin-dsl — (1) we enable the support for writing convention plugins as build files in buildSrc. Which are the convention plugins we already inspected above. Furthermore, we add Gradle’s plugin portal as repository (2), which gives us access to community plugins. To use a plugin it needs to be declared as dependency in the dependencies {} block.

Apart from the Gradle build files, you can find example Java source code and test source code in the corresponding folders. Feel free to modify these generated sources and tests to explore how Gradle reacts to changes when running the build as described next.

Run the tests

You can use ./gradlew check to execute all tests in all subprojects. When you call Gradle with a plain task name like check, the task will be executed for all subprojects that provide it. To target only a specific subproject, you can use the full path to the task. For example :app:check will only execute the tests of the app project. However, the other subprojects will still be compiled in the case of this example, because app declares dependencies to them.

$ ./gradlew check

BUILD SUCCESSFUL
9 actionable tasks: 9 executed

Gradle won’t print more output to the console if all tests passed successfully. You can find the test reports in the <subproject>/build/reports folders. Feel free to change some of the example code or tests and rerun check to see what happens if a test fails.

Run the application

Thanks to the application plugin, you can run the application directly from the command line. The run task tells Gradle to execute the main method in the class assigned to the mainClass property.

$ ./gradlew run

> Task :app:run
Hello world!

BUILD SUCCESSFUL
2 actionable tasks: 2 executed
The first time you run the wrapper script, gradlew, there may be a delay while that version of gradle is downloaded and stored locally in your ~/.gradle/wrapper/dists folder.

Bundle the application

The application plugin also bundles the application, with all its dependencies, for you. The archive will also contain a script to start the application with a single command.

$ ./gradlew build

BUILD SUCCESSFUL in 0s
7 actionable tasks: 7 executed

If you run a full build as shown above, Gradle will have produced the archive in two formats for you: app/build/distributions/app.tar and app/build/distributions/app.zip.

Publish a Build Scan

The best way to learn more about what your build is doing behind the scenes, is to publish a build scan. To do so, just run Gradle with the --scan flag.

$ ./gradlew build --scan

BUILD SUCCESSFUL in 0s
7 actionable tasks: 7 executed

Publishing a build scan to scans.gradle.com requires accepting the Gradle Terms of Service defined at https://gradle.com/terms-of-service.
Do you accept these terms? [yes, no] yes

Gradle Terms of Service accepted.

Publishing build scan...
https://gradle.com/s/5u4w3gxeurtd2

Click the link and explore which tasks where executed, which dependencies where downloaded and many more details!

Summary

That’s it! You’ve now successfully configured and built a Java application project with Gradle. You’ve learned how to:

  • Initialize a project that produces a Java application

  • Create a modular software project by combining multiple subprojects

  • Share build configuration logic between subprojects using convention plugins in buildSrc

  • Run similar named tasks in all subprojects

  • Run a task in a specific subproject

  • Build, bundle and run the application

Next steps

When your project grows, you might be interested in more details how to configure JVM projects, structuring multi-project builds and dependency management: