Gradle, multi-project builds explained - DO OK

Gradle, multi-project builds explained

In this article, I will try to cover some of the most used Gradle features. I will be focusing on multi-project builds since this is the most common type of Gradle build you will ever see. There will be quite a lot of code to help you understand the concepts and solutions. Keeping things short let’s skip to the important part.

Gradle is a convenient, robust and powerful build tool adopted by many companies across the world. It uses a declarative syntax to describe small and simple tasks, as well as build processes of large, complicated, multi-project applications. Gradle is known mostly for it’s easy to read DSL language, elastic structure and handy dependency management. Another very interesting and useful fact is Gradle’s ability to operate in Kotlin besides Groovy. As Kotlin popularity arises it can be an important argument. For the full list of features, I suggest visiting Gradle official site https://gradle.org/features/. Gradle is so popular, that every Java programmer (and plenty of others) at some point of his career will be facing Gradle build files. It’s good to be prepared for this, as a properly implemented build process can save a lot of nerves and plenty of time for every project team member. Being prepared is also important because of many possible traps in every building process.

Why should you bother?

Firstly I owe you a bit of explanation. Across this article, I will never use the term “multi-module build” as a synonym to “multi-project build”. In real life, you will probably encounter both of these terms but technically speaking “multi-project build” is correct. Gradle is very clear in this matter and is using this term in every place of its documentation. It’s now, more than ever, important to keep things clear, as Java 9 modules are around. Overloading terms is never a good idea when clarity is needed. So why are multi-project builds a thing? Why can’t we just build a few applications in separation and ship them to the client? The answer is just the same as for many other programming related questions - dependency management. Whenever few of your projects depend on each other you will need to build, ship and release them together in a convenient manner. This is, in my opinion, the strongest premise to construct a multi-project build process. One can also ask “Why not just merge these projects into one?” and it will be a valid question. There are many arguments against having one big project, especially in the time of micro-services. With separate projects, you will have your logic and domains separated from each other which is very helpful in the process of software development. There is no need for the whole team to know all projects and there are smaller chances of having a team member working on the same part of the code. This is one of the best ways to improve your team productivity and to make your development process more parallel. Moreover separating logic into different projects improves code readability and helps you avoid code duplication.

With that being said I will assume that you want to check what Gradle has to offer.

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, 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 files 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 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 solution 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 a 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 few tricks can greatly improve your and your team overall experience. Being wise in terms of the build process and treating build scripts as first-class citizens is an investment with high return.

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

What are the Five Phases in Software Delivery
In February 2020 PMI published
03.12.2020, min read
Marta Maciaszek
Read more
DO OK named among Deloitte’s Technology Fast 50 Central Europe 2020
DO OK has been ranked 34th in the Deloitte Technology Fast 50 Central Europe with 710%...
26.11.2020, min read
Mihail Yarashuk
Read more
Role of Project Manager in Software Development
Project management becomes a critical aspect of the delivery and role of a Project Mana...
25.11.2020, min read
Marta Maciaszek
Read more
Cookies

Our website has cookies. more info