idiomatic-gradle
This project shows how to use Gradle in a structured way not only for simple, but also complex project setups.
[!TIP] Learn more about all the Gradle features used in this project:
- 🎞️ Understanding Gradle video series
There are two sister repositories that share the same example with a different focus in the build setup:
- 🧶 gradle-project-setup-howto Focus on a full mono-repo setup
- 🕹️ javarcade Focus on structured dependency management with the JavaRCA recipe
This project uses Gradle's Kotlin DSL for configuration files. However, you can use any JVM language (including pure Java!) to configure Gradle. More details on that can be found here:
- 🔌 gradle-plugins-howto Details on the plugin concept alternative implementation solutions
This example uses the
build.gradle.ktsfiles as dependency definition files. An alternative approach is to usemodule-info.javafiles, as part of the Java Module System (JPMS), to define dependencies directly in Java instead of Gradle-specific Kotlin notation. More details on that:
- 🧩 javarca.de Java Recipe for Carefree dependency Administration
Example
This example is a software composed of three products:
- A game engine
- A graphics renderer to plug into the engine
- A game called jamcatch based on the engine
Each product consists of one or multiple modules.
To explore and build the example, clone this repository and open the root folder in IntelliJ IDEA. You can also build from the command line by running gradlew.
./gradlew
All three products are located in this Git repository (monorepo approach). They could also be moved into separate Git repositories (multirepo approach) with the same build setup. To demonstrate that, the products are also published to a Maven repository. Then, each product can also be built on its own by consuming other published products from the Maven repository. You can explore that by opening one of the individual product folders (engine, renderer, jamcatch) in IntelliJ IDEA.
Individual Modules - Production Code and Tests
Inside the three product folders, there are multiple module folders:
├── engine
│ ├── javarca-engine
│ ├── javarca-model
├── renderer
│ └── renderer-lwjgl
└── jamcatch
├── jamcatch-actors
├── jamcatch-assets
└── jamcatch-stage
Each module folder contains:
- Production code and tests (Java, independent of Gradle)
- A build.gradle.kts file that only contains
a
pluginsanddependenciesblock. These files are used to clearly define the dependencies of that module to other modules of the same (or other) products and to third-party open source modules. They are explicitly not used for further build configuration (see JavaRCA recipe for more details on that).
└── jamcatch
└── jamcatch-actors
├── src
│ ├── main/java
│ ├── test/java
│ └── testEnd2end/java
└── build.gradle.kts
Each module contains unit tests using Gradle's default setup for Java projects with the src/test/java folder.
Furthermore, the modules of the jamcatch product also have end2end tests that run the module in the
context of a complete application. This demonstrates the flexible testing and dependency management capabilities
of Gradle that allow you to define custom tests sets with individual dependencies.
Technically, each product is a Gradle build (has its own settings.gradle.kts file) and each module is a Gradle subproject (has its own build.gradle.kts file for dependency declaration). All modules are organized as independent, first-class entities under a shared product directory. Inside a product, shared code should live in a clearly defined common module (like engine/javarca-model in the example).
[!WARNING] Putting comment code into a parent folder is an anti-pattern!
└── engine/ ├── src/... // common code (do not do this!) ├── javarca-engine │ └── src/.. └── javarca-feature-b └── src/...
Composing Applications and Reports
There are separate Gradle subproject for composing individual applications of reports from the modules. These are so-called aggregation subproject that do not contain any code themselves. They only have a build.gradle.kts file to define the dependencies to be aggregated.
└── aggregation
├── app-jamcatch
│ └── build.gradle.kts
├── app-jamcatch-debug
│ └── build.gradle.kts
└── reports
└── build.gradle.kts
To run an application:
./gradlew :aggregation:app-jamcatch:run
You can modify the dependencies in build.gradle.kts and run again to explore the aggregation topic.
Idiomatic Build Logic Structure
All individual Gradle build configurations are located in gradle-conventions.
It contains some standard configuration for Java compilation and testing
and more individual configurations for publishing and the end2end testing setup.
To organize this configuration into multiple files, Gradle provides the concept of
Convention Plugins.
Here, individual .gradle.kts files for different configuration aspects can be written and then treated as plugins.
This allows reusing and composing the configuration aspects in a flexible way.
[!Caution] With this, the following outdated practices are avoided:
- No direct dependencies between tasks declared (except for extending lifecycle tasks like
assembleorcheck)- No direct dependencies between tasks from different subprojects are declared
- No cross-project configuration (subproject / allprojects) is performed
- Each build script of a subproject is simpler to read as all relationships to other projects are expressed in terms of dependencies
Treating all configurations as plugins, also allows publishing them to a Maven repository. Technically, gradle-conventions is also a Gradle build just as our products are. In this case, though, it is a build producing Gradle plugins, rather than modules of our software.
Discussions and alternative structuring options
- Using or not using a Version Catalog
- Folder name/location for convention plugins
- Name of Version Catalog TOML file
- Name of 'Settings Plugin' project
- Using JVM packages for precompiled script plugins
More questions or points you would like to discuss? Please open an issue.
FAQ
- buildSrc vs. Composite Build for convention plugins
- Where to put the Gradle Wrapper
- How to use an external plugin in a local plugin and where to put its version
- Android Studio: 'gradle/plugins' project clean uses different Gradle version
More questions or points you would like to discuss? Please open an issue.
