Building a Monorepo with Gradle

26 minute read     Updated:

Rajkumar Venkatasamy %
Rajkumar Venkatasamy

This article discusses the management of Gradle monorepos. If monorepos are your thing, Earthly might be worth a look. Earthly significantly enhances build efficiency in monorepos. Check it out.

Gradle is a powerful and flexible build tool used for code compilation, testing, and deployment. It’s widely used in Java and Android ecosystems but supports many other languages as well, including Kotlin, C/C++, and JavaScript.

One of Gradle’s key features is its ability to handle monorepos efficiently. A monorepo is a repository that includes multiple distinct projects within its structure. This approach offers several advantages, including facilitating code sharing, collaboration, and reuse among projects, which ultimately reduces redundancy and promotes consistency.

Thanks to its cross-platform compatibility, Gradle allows seamless usage across diverse operating systems. Other noteworthy features include the build cache, which significantly reduces build times by reusing task outputs from previous builds and is particularly beneficial in large monorepo projects. Additionally, Gradle effectively manages dependencies from various sources and supports conflict resolution, transitive dependencies, and project dependencies—important for monorepos with intricate project dependencies.

In this tutorial, you’ll learn how to create and manage a monorepo using Gradle.

Create and Manage a Monorepo with Gradle

Before you begin this tutorial, make sure you have the following prerequisites:

  • Gradle installed and ready for use (make sure your version is 8.5 or newer)
  • An IDE for working with a Java application
  • The Java JDK (version 8 or newer)

In this scenario, you’ll be working with an imaginary e-commerce company called “ZZZ,” whose backend engineering team is tasked with designing and delivering a backend project with a Gradle-based monorepo setup. You’ll create and manage three subprojects within a single monorepo setup using Gradle. Among the three subprojects, subproject-2 depends on subproject-1, and subproject-3 depends on subproject-2:

Monorepo project directory structure, courtesy of Rajkumar Venkatasamy

In the diagram above:

  • The monorepo project directory is the root directory of the monorepo. It contains all of the subprojects and other files related to the project.
  • The settings.gradle file is used to configure Gradle for the monorepo. It tells Gradle which subprojects are part of the monorepo and where they are located.
  • Each subproject is a separate project with its own build.gradle file and source code. Subprojects can be applications, libraries, or other types of projects.
  • The build.gradle file is used to configure Gradle for each subproject. It tells Gradle how to build each subproject and what dependencies it has.
  • Each src directory contains the source code for the corresponding subproject.
  • Gradle tasks are the tasks that Gradle can run to build, test, and deploy the subproject.

In terms of dependencies, subproject-2 depends on subproject-1, which means that subproject-1 needs to be built before subproject-2. Similarly, subproject-3 depends on subproject-2, which needs to be built before subproject-3. All these subprojects will be developed and bundled in such a way that they work together to print a greeting message.

Create a Monorepo Project Directory

The first step to building a monorepo with Gradle is to create a monorepo project directory. Here, it’s named building-a-monorepo-with-gradle. This directory will contain all of the subprojects and other files related to the project.

Execute the following command in the terminal from the project directory to initialize a basic Gradle project:

gradle init

An interactive terminal will appear. Select the following:

Starting a Gradle Daemon (subsequent builds will be faster)

Select type of project to generate:
  1: basic
  2: application
  3: library
  4: Gradle plugin
Enter selection (default: basic) [1..4] 1

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

Project name (default: building-a-monorepo-with-gradle):

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


> Task :init
To learn more about Gradle by exploring our Samples \ 
at https://docs.gradle.org/8.5/samples

BUILD SUCCESSFUL in 2m 33s
2 actionable tasks: 2 executed

If successful, you should see the following files in the root project directory:

.gitattributes
.gitignore
.gradle
build.gradle
gradle
gradlew
gradlew.bat
settings.gradle

Create Subprojects

To create subprojects, navigate to the project root directory and create directories for each of the subprojects using the following command:

mkdir subproject-1 subproject-2 subproject-3

Then, edit the settings.gradle file and paste the following at the end of the file:

include 'subproject-1', 'subproject-2', 'subproject-3'

This statement ensures that the newly created subproject directories are considered part of the monorepo project. To verify this, execute the following command in the terminal:

gradle -q projects

Your output should look like this:

------------------------------------------------------------
Root project 'monorepo-gradle'
------------------------------------------------------------

Root project 'monorepo-gradle'
+--- Project ':subproject-1'
+--- Project ':subproject-2'
\--- Project ':subproject-3'

Develop Subprojects

Now that the directories for the subprojects are created and included as part of Gradle’s monorepo project, it’s time to develop the three subprojects.

Create Subproject-1

Start by creating a build.gradle file in the subproject-1 directory. Then, edit that file to include the following syntax:

dependencies {

}

Subproject-1 serves as a library and does not depend on other subprojects, so you can leave the dependencies section empty.

Next, create the directory structure for developing the source code using the following command:

cd subproject-1
mkdir src\main\java\subproject\

Note: This tutorial was prepared on a Windows-based machine, which is why you see slashes. mkdir does not include the -p flag to create the new directory structure with subdirectories in it. Feel free to change the command as per your machine’s operating system.

Create a Java file named Library.java in the newly created subproject directory and paste the following code into it:

package subproject;

public class Library {
    public static String getMessage() {
        return "Hello from Library!";
    }
}

This code defines a class named Library and prints the message Hello from Library! when the getMessage function is called. Later, you’ll call this library function from subproject-2, which depends on subproject-1.

Create Subproject-2

For subproject-2, create a build.gradle file in the subproject-2 directory, then edit that file to include the following syntax:

dependencies {
    implementation project(":subproject-1")
}

As previously stated, subproject-1 serves as a library, and subproject-2 depends on subproject-1. That’s why the dependencies section is filled in.

Next, create the directory structure for developing the source code using the following command:

cd subproject-2
mkdir src\main\java\subproject\

Create a Java file named Service.java in the newly created subproject directory and paste the following code into it:

package subproject;


public class Service {
    public String serve() {
        return Library.getMessage();
    }
}

This code defines a class named Service and has a function called serve. This function in turn calls subproject-1’s getMessage method via the previously defined Library class. Later, you’ll call the serve function from subproject-3.

Create Subproject-3

For subproject-3, create a build.gradle file in the subproject-3 directory, then edit that file to include the following syntax:

mainClassName = 'subproject.App'

dependencies {
    implementation project(":subproject-2")
}

// Custom run task
task runApp(type: JavaExec) {
    mainClass = mainClassName
    classpath = sourceSets.main.runtimeClasspath + sourceSets.main.output
}

Subproject-3 is an app that depends on subproject-2. Because subproject-3 is an application that needs to be run to generate the greeting message, other sections of the build.gradle code call out the main application class file name (which you’ll create soon) and the custom runApp task.

The runApp task is used to execute the Java application within Gradle. It specifies the main class to run and configures the classpath to include both project-specific classes and runtime dependencies. This enables convenient and integrated execution within Gradle’s build process, offering flexibility and control over launching the application.

Create the directory structure for developing the source code using the following command:

cd subproject-3
mkdir src\main\java\subproject\

Create a Java file named App.java in the newly created subproject directory and paste the following code into it:

package subproject;

public class App {
    public String getGreeting() {
        Service service = new Service();
        return service.serve();
    }

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

This code defines a class named App and has a function called getGreeting. This function in turn calls subproject-2’s serve method via the previously defined Service class. This serve function, in turn, calls subproject-1’s getMessage function, completing the whole cycle of dependency.

The final structure of your subprojects should look like this:

Subprojects structure

Build the Monorepo Project Using Gradle

Now that all of the subprojects are ready, it’s time to build them using Gradle. However, before you can do that, you must prepare the build.gradle of the monorepo project to define the shared configurations of all the subprojects. If you don’t prepare this file, you’ll encounter errors when you run the gradle build command from the monorepo’s root project directory.

To prepare the build.gradle file in the building-a-monorepo-with-gradle directory, add the following content:

def javaProjects = [
        project(":subproject-1"),
        project(":subproject-2"),
        project(":subproject-3"),
]

configure(javaProjects) {
    println "${project.name}"
    apply plugin: 'application'
    version '1.0.0'
    repositories {
        mavenCentral()
    }

    dependencies {

    }

    test {
        useJUnitPlatform()
    }
}

This code defines the configuration applicable for each subproject and enables each subproject to be built and run as a standalone application. It also sets a common version and utilizes Maven Central for dependency management.

Then, trigger the build process of the entire monorepo project using the following command:

cd building-a-monorepo-with-gradle
gradle build

All subprojects are built with the execution of a single command, which exemplifies how easy it is to manage a monorepo project with Gradle.

The output of the build command should look like this:

> Configure project :                   
subproject-1                            
subproject-2                            
subproject-3                            

BUILD SUCCESSFUL in 1s
15 actionable tasks: 15 executed

You should also see a build directory holding the output of the gradle build command in each of these subprojects:

Subprojects structure after the build

If you rerun the same gradle build command, your output will look like this:

> Configure project :
subproject-1
subproject-2
subproject-3

BUILD SUCCESSFUL in 1s
15 actionable tasks: 15 up-to-date

Notice the last statement in the output: 15 actionable tasks: 15 up-to-date. Before, it was 15 actionable tasks: 15 executed. The difference is that during the second invocation of the gradle build command, Gradle by default uses the cache and skips certain steps (eg compilation) to improve the build cycle time.

Run the App

Now that the build is ready, run the app with the following command:

cd building-a-monorepo-with-gradle
gradle :subproject-3:runApp

This command calls the custom task runApp that you defined previously in subproject-3, which is the app containing the main class. Your output should look like this:

> Configure project :
subproject-1
subproject-2
subproject-3

> Task :subproject-3:runApp
Hello from Library!

Notice that the message from subproject-1 is getting printed via the calls passed across multiple subprojects involved in this monorepo project.

Go ahead and change the greeting message in subproject-1’s Library.java class from Hello from Library! to Hello from Subproject-1! as follows:

package subproject;

public class Library {
    public static String getMessage() {
        return "Hello from Subproject-1!";
    }
}

Then, execute the following command to build the projects:

cd building-a-monorepo-with-gradle
gradle build

Your output should look like this:

> Configure project :
subproject-1
subproject-2
subproject-3

BUILD SUCCESSFUL in 1s
15 actionable tasks: 11 executed, 4 up-to-date

Notice that the last statement in the output now says 15 actionable tasks: 11 executed, 4 up-to-date, meaning that due to the changes made in the source code file of subproject-1, the Gradle tool has performed eleven tasks while skipping four tasks and relying upon the cache for those tasks.

This shows how Gradle’s dependency management works at a high level when one of the dependencies gets updated and how it affects the build chain or cache usage process.

Conclusion

In this tutorial, you learned how to create and manage a monorepo using Gradle with multiple Java-based subprojects.

As your mono-repo expands, build times can grow, and at that point, it can make sense to look at solutions for keeping mono-repo build times under control.

Also, if you’re wrestling with Java monorepos, Earthly could be your new best friend. Imagine this: a tool that knows exactly what needs to be built and what doesn’t. Earthly simplifies Java monorepos with content addressing and an auto-skip feature. It uses a build graph to show the build steps and their dependencies, enhancing performance and reliability by running steps in parallel and skipping unnecessary ones. Try it out for free today.

The tutorial’s source code is available in this GitHub repository.

Earthly Cloud: Consistent, Fast Builds, Any CI
Consistent, repeatable builds across all environments. Advanced caching for faster builds. Easy integration with any CI. 6,000 build minutes per month included.

Get Started Free

Rajkumar Venkatasamy %
Rajkumar Venkatasamy
Rajkumar has nearly sixteen years of experience in the software industry as a developer, data modeler, tester, project lead, product consultant, data architect, ETL specialist, and technical architect. Currently he is a principal architect at an MNC. He has hands-on experience in various technologies, tools, and libraries.

Published:

Get notified about new articles!
We won't send you spam. Unsubscribe at any time.