idiomatic-gradle

More: Author   ReportBugs   
Tags:

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:

There are two sister repositories that share the same example with a different focus in the build setup:

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:

This example uses the build.gradle.kts files as dependency definition files. An alternative approach is to use module-info.java files, 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:

  1. A game engine
  2. A graphics renderer to plug into the engine
  3. 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 plugins and dependencies block. 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 assemble or check)
  • 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

More questions or points you would like to discuss? Please open an issue.

FAQ

More questions or points you would like to discuss? Please open an issue.

Apps
About Me
GitHub: Trinea
Facebook: Dev Tools