Build Tool - Gradle β
Gradle was chosen as the build tool for this project because it better meets the needs of a modular, scalable, CI/CD automation-oriented project than alternatives like Maven.
π οΈ Concepts Used in the Project β
ποΈ Automatic Detection of New Gradle Modules β
The project uses Gradle automatic multi-project includes to:
- Automatically detects new modules placed inside
tech-starters/backend/ordrinkit/. - Avoids manual maintenance of the
settings.gradle.ktsfile. - Enables effortless scalability, useful for hexagonal or modular architectures.
π¦ Version Catalog β
To centralize and standardize dependency version management, we use Version Catalog with two TOML files:
libs.versions.toml β
- Contains versions of dependencies used at runtime and for testing.
- Centralizes versions for
jooq,junit, etc.
pluginLibs.versions.toml β
- Contains versions of plugins used during compilation (e.g.,
kotlin,spring-boot, ...). - Allows clean plugin version management without duplication across
build.gradle.ktsfiles.
Benefits:
β
Centralized and visible version management
β
Easy version updates in a single location
β
Consistent dependency alignment across all modules
π Internal Dependency Management (BOM) β
We use a Gradle Platform, gradle/platform, to ensure consistent dependency versions across all modules. It imports the Spring Boot, Spring AI and Spring Cloud GCP BOMs, then pins the versions of the version catalog on top of them.
- Define an internal BOM for aligning dependency versions across all modules.
- Reduce version drift and prevent runtime errors due to incompatibilities.
- Expose declarative dependency constraints for external consumers.
π§© Gradle Conventions β
To maintain consistency, we use modular Gradle Conventions to factor shared configurations across modules:
The conventions live in the build-logic included build, grouped in folders for readability only: a plugin id comes from the file name. Three of them are archetypes, and a Kotlin module applies one of them plus the add-ons it needs. The contract module applies openapi-contract-convention alone, and gradle/platform is a Java platform.
common-convention β
- Applied by every Kotlin module, directly or through the other two archetypes.
- Sets the Java toolchain and the Kotlin compilation, constrains every source set with the platform, and applies
code-analysis-conventionandtest-convention.
library-convention β
- Used for library projects (domain, infrastructure, tech starters).
- Adds
common-convention,documentation-convention, Spring context and transactions, and the event sourcing and kotlin starters.
api-convention β
- Used for Spring Boot applications.
- Adds
common-convention,documentation-convention,spring-boot-starter-webmvc, the kotlin and monitoring starters, build and git info for Actuator, GraalVM native builds, and dependency locking.
test / test-fixtures-convention β
test-conventionruns tests on the JUnit Platform and gives every Kotlin moduletest-starter: JUnit Jupiter, Kotest assertions, kotlin-faker, Spring Boot test and Testcontainers support.test-fixtures-conventionadds thetestFixturessource set, where a module shares its test doubles.
jooq-codegen-convention β
- Generates the jOOQ classes from a running PostgreSQL into
src/generated/jooq/kotlin, which is committed. Only an explicitjooqCodegenruns it, never the build.
documentation-convention β
- Runs the KSP processor of
documentation-starter, which writes the domain and tech starter pages of this site. Only an explicitkspKotlinruns it, never the build.
openapi-contract-convention β
- Defines a standard project structure for OpenAPI contracts.
contract-first-convention β
- Configures server code generation from OpenAPI contracts.
- Integrates generation tools (
openapi-generator) to automatically produce server code (controllers, models, delegates, ...) aligned with the contracts.
code-analysis-convention β
Applies detekt to every module, as the single authority on static analysis β code smells through its own rule sets, formatting through its ktlint ruleset.
detektAllruns every enabled detekt task of a project, andcheckruns them too. The same convention lands on the modules and on the root project, and what it enables follows from where: on a module,detektMain,detektTestanddetektTestFixtures, the three that resolve types β the others run without a compiled classpath, so any rule needing type information silently never fires. On the root project, which has no source set, the plaindetekttask pointed at every*.gradle.ktsin the repository, build-logic's included, which no source set contains.-Pdetekt.autoCorrect=truefixes what can be fixed, formatting included. The property exists rather than detekt's own--auto-correctflag because that flag is a per-task Gradle option: on a command line naming several tasks it binds only to the one it follows, leaving the others silently in report-only mode. TheautoCorrect: trueentries indetekt.ymlonly declare which rules are allowed to rewrite code.detektReportMergeSarifmerges the per-module SARIF reports into a single file, which the CI uploads to GitHub code scanning. Without the merge the upload is impossible, GitHub taking only a handful of SARIF files per category.- Generated code (JOOQ under
src/generated, OpenAPI underbuild/) is excluded. - Any finding fails the build β
failOnSeverityis tightened from detekt's defaultErrordown toInfo.
A single file configures it: code-analysis/detekt/detekt.yml, holding only this project's deviations from detekt's defaults, the convention setting buildUponDefaultConfig. There are four: the rules this project opts into out of the 108 detekt ships inactive. Everything else is detekt's own default, test sources included β **/testFixtures/** is held to the same standard as production code, which is what detekt does by default and costs twelve findings.
code-analysis/detekt/baseline.xml holds the 82 findings that predate the switch to a blocking build. They no longer fail anything; everything new does. The file only ever shrinks β you delete a line when you fix what it holds.
The detektBaseline* tasks write one file per source set rather than to that path, so regenerating wholesale means merging their output back into it. DetektCreateBaselineTask does not extend Detekt either, so the convention applies the generated-code exclusion to it separately: without that it records the JOOQ and OpenAPI output too, 1117 entries instead of 82.
WARNING
detekt's ktlint ruleset reads detekt's configuration, not .editorconfig. The root .editorconfig only keeps IntelliJ's own formatter aligned β the detekt IDE plugin annotates and offers a manual auto-correct action, but does not format on save β so the values the two files share, indentation and line length, have to be kept in agreement by hand.
A pre-commit hook under .githooks/ formats staged Kotlin files, enabled per clone with git config core.hooksPath .githooks.
ide-convention β
Declares the IntelliJ settings in the build instead of committing .idea, which is gitignored. IntelliJ regenerates them at every Gradle sync: the detekt and EditorConfig plugins are marked as required, the SQL dialect is set to PostgreSQL, and Build/Run actions are delegated to Gradle.
Disabled
The convention is kept in build-logic, commented out and applied nowhere. On Gradle 9.8.0, name.remal.idea-settings 4.0.9 makes every build that stores the configuration cache exit 1 without output, so any first run of a command, and every CI build, would fail. Applying it only during an IntelliJ sync was tried and rejected: IntelliJ then asks for a system property that a newcomer cannot guess. It comes back once a fixed plugin version is released. Until then, install the detekt IntelliJ plugin by hand.
Once re-enabled, it belongs to the root build.gradle.kts, which exists for exactly this: idea-ext configures idea.project, an extension that lives only on the root project. Gradle's own guidance calls the root build file "the place to configure some settings and conventions that apply globally to the entire build, that are not configured via Settings" β IDE settings being precisely that. What does not belong there is anything touching source code: the root project has none, and module concerns go through the conventions.
.idea/detekt.xml is the one versioned exception: it points the IntelliJ detekt plugin at the project's own config, and idea-settings has no DSL for it.
TIP
More Gradle best practices here