Gradle, multi-project builds explained - DO OK

Gradle, multi-project builds explained

In this article, I will aim to explore and explain some of the most frequently used Gradle features. My focus will primarily be on multi-project builds, as this is the prevalent type of Gradle build you encounter. To facilitate comprehension, I'll provide substantial code examples to illustrate the concepts and solutions. Without further ado, let's dive into the critical aspects.

Gradle is a versatile, robust, and powerful build tool that has been widely adopted by companies worldwide. It leverages a declarative syntax to define both simple tasks and complex build processes, making it suitable for large, intricate, multi-project applications. Gradle is renowned for its readable DSL language, flexible structure, and effective dependency management. Another noteworthy aspect is Gradle's compatibility with Kotlin in addition to Groovy, which becomes increasingly relevant with Kotlin's growing popularity. You can explore the complete list of features on the official Gradle website at https://gradle.org/features.

 

Gradle's popularity is such that virtually every Java programmer, and many others, will encounter Gradle build files at some point in their careers. Being prepared for this is essential, as a well-implemented build process can save time and reduce stress for every member of a project team. Additionally, preparedness is crucial due to the potential pitfalls that can arise in any building process.

Why should you bother?

Firstly, I'd like to provide a bit of clarification. Throughout this article, I will refrain from using the term "multi-module build" as a synonym for "multi-project build." While you may encounter both terms in practice, it's important to note that "multi-project build" is the technically correct term. Gradle is unambiguous in this regard and employs "multi-project build" consistently in its documentation. Clarity is paramount, especially now with the advent of Java 9 modules. Overloading terms can lead to confusion when precision is required.

 

Now, why are multi-project builds significant? Why can't we simply build separate applications and deliver them independently to the client? The answer lies in dependency management. Whenever several of your projects rely on each other, you'll need to build, package, and release them together in a cohesive manner. In my view, this is the most compelling reason to adopt a multi-project build process.

 

You might wonder, "Why not just merge these projects into one?" This is a valid question, and there are numerous arguments against consolidating everything into a single large project, particularly in the era of microservices. By keeping projects separate, you can maintain logical and domain separation, which is advantageous during software development. The entire team doesn't have to be familiar with all projects, and the chances of team members working on the same section of code are reduced. This approach enhances team productivity and facilitates parallel development. Additionally, segregating logic into distinct projects enhances code readability and helps prevent code duplication.

 

With this context in mind, I assume you are interested in exploring what Gradle has to offer in this context.

Basics

In the most common scenario, the Gradle project has two types of interesting files. The first one is commonly placed in the root project directory and called settings.gradle.kts (keep in mind that location is not a requirement, and neither is the name). In a single project build, it will most probably consist of just a project name. It’s a place for you to declare all project-scoped settings and include your projects to build.

rootProject.name = "donut_factory"
include("hole_maker", "sugar_producer", "happiness_provider")

settings.gradle.kts

The second type of file is the so-called build file. There should be one file for each project, placed in respective directories, and one root file placed in a root directory. The root build file is usually used to share some of the most common configurations, while each particular build file will handle the build process of its respective project.

plugins {
    java
    id("org.springframework.boot")
}

repositories {
    mavenCentral()
    jcenter()
}

dependencies {
    annotationProcessor(group = "org.projectlombok", name = "lombok", version = lombokVersion)

    implementation(group = "org.springframework.boot", name = "spring-boot-starter")
    implementation(group = "org.springframework.boot", name = "spring-boot-starter-web")
    implementation(group = "org.springframework.boot", name = "spring-boot-starter-data-jpa")
    implementation(group = "org.springframework.boot", name = "spring-boot-starter-security")
    implementation(group = "org.postgresql", name = "postgresql", version = postgreSqlVersion)
    implementation(group = "javax.inject", name = "javax.inject", version = javaxInjectVersion)

    implementation(project(":hole_maker"))
    implementation(project(":sugar_producer"))
    implementation(project(":happiness_provider"))

    compileOnly(group = "org.projectlombok", name = "lombok", version = lombokVersion)

    testImplementation(group = "org.junit.jupiter", name = "junit-jupiter-api", version = junitVersion)
    testImplementation(group = "org.assertj", name = "assertj-core", version = assertjVersion)
    testImplementation(group = "org.springframework.boot", name = "spring-boot-starter-test")
    testImplementation(group = "org.springframework.security", name = "spring-security-test")
    testImplementation(group = "org.mockito", name = "mockito-junit-jupiter", version = mockitoVersion)

    testAnnotationProcessor(group = "org.projectlombok", name = "lombok", version = lombokVersion)
    testRuntimeOnly(group = "org.junit.jupiter", name = "junit-jupiter-engine", version = junitVersion)
    testCompileOnly(group = "org.projectlombok", name = "lombok", version = lombokVersion)
}

root build.gradle.kts

As you can see there is no magic there. I will focus on the multi-project part ignoring the rest. Besides declaring projects in settings.gradle.kts file you will also need to tell Gradle which project depends on which. It’s no different than declaring external dependency, except the project() part which is responsible for creating a dependency on your project without adding it to a configuration (this was already done earlier). That’s pretty much all about basics. You can now build your projects together.

Shared logic and resources

There are two common types of shared resources in a build process: logic and properties. Each of them has a proper place. Shared properties should be stored in a special file named gradle.properties which is designed as a handy config file. You can always fetch them to your code with a helpful groovy function. One thing to be aware of - properties should always be placed in a proper project properties file and each project should have its own file.

org.gradle.caching=true
org.gradle.console=rich
org.gradle.logging.level=lifecycle
org.gradle.parallel=true
com.example.donut_factory.hole.size=big

gradle.properties

When it comes to shared logic, the solution is obvious but very underestimated. Gradle provides you with a custom system designed to make your life easier. You just need to create a root-level directory named buildSrc. Since now, you can gather your shared logic inside this directory and use it in all your build scripts. It’s compiled before your scripts and available within them to use. You can also create the standard src/main/java directories and store shared code there. It can be easily tested and maintained and you have your IDE help there. In a multi-project build, there can be only one buildSrc directory and it needs to be placed in the root project directory.

repositories {
    mavenCentral()
}

dependencies {
    testImplementation("junit:junit:4.12")
}

buildSrc build.gradle.kts

Manage your dependencies with grace

Dependency management is crucial when speaking about multi-project builds. You can easily end up with an unmaintainable build file with a lot of dependencies. This is not only a clear code issue but also an opportunity to speed up your build, as fetching unnecessary dependencies can slow it down a lot. Fortunately Gradle, again, has some very handy solutions to offer and it will, again, be our known friend - buildSrc directory. Having the possibility to treat it as normal code allows us to shape dependencies in our own, often very elaborated, way. There are two most common ways to improve the build script: transforming dependencies into constant fields of an object and preparing a different object for different types of dependencies. Having properly named and grouped dependencies is a half-success. Moreover, you will have the help of your IDE, which will greatly improve your experience.

object Versions {
    val junit = "2.3"
    val pumpkin = "5.6"
}

object TestingLibraries {
    val junit = "org.junit.jupiter${Versions.junit}"
}

object FoodLibraries {
    val pumpkin = "de.pumpkin.world${Versions.pumpkin}"
}

dependencies.kts

implementation FoodLibraries.pumpkin

testImplementation TestingLibraries.junit

build.gradle.kts

Conclusion

It’s always good to know your tool, especially when it accounts for limiting your productive time. You don’t always need to be a Gradle expert, but knowing basic concepts and a few tricks can greatly improve your and your team's overall experience. Being wise in terms of the build process and treating build scripts as first-class citizens is an investment with a high return.

Have you got any tips or tricks to make your work with Gradle a pleasure? Share with us in the comments!

DO OK is part of the top 7% global software engineering vendors on Pangea
We are proud to announce that DO OK is now a verified ...
25.11.2022, min read
Anca Papainog
Read more
Time to market is more critical than in-house engineering
What is near-sourcing? How to start a development project fast without in-house enginee...
22.03.2022, min read
Dmitrij Żatuchin
Read more
DO OK Recognized as One of Estonia’s Most Reviewed IT Services Companies by The Manifest
Our team is among the few companies that were listed as the most reviewed IT services i...
06.12.2021, min read
Mihail Yarashuk
Read more
Cookies

Our website has cookies. more info

EU Flag