Rule Writing

AI Rules for Groovy and Gradle Builds

AI generates Gradle builds with deprecated APIs, hardcoded versions, and Groovy DSL when Kotlin DSL is preferred. Rules for modern, maintainable build scripts.

7 min read·March 19, 2025

AI generates Gradle with deprecated APIs and hardcoded versions from 2018

Version catalogs, convention plugins, configuration avoidance, and Kotlin DSL

Why Gradle Build Scripts Need AI Rules

Gradle is the build system for Android, Spring Boot, and most JVM projects — and it's one of the most complex tools AI assistants interact with. The API surface is enormous, deprecated patterns persist in training data for years, and the difference between a well-configured and a poorly-configured Gradle build is 10x in build time and maintainability.

The most common AI failures: using Groovy DSL when the project uses Kotlin DSL (or vice versa), hardcoding dependency versions instead of using version catalogs, using eager task configuration instead of lazy configuration avoidance, ignoring the build cache, and generating deprecated API calls (compile instead of implementation, runtime instead of runtimeOnly).

These rules cover both Groovy DSL and Kotlin DSL. Specify which your project uses — the syntax is significantly different and the AI needs to know which to generate.

Rule 1: Kotlin DSL Over Groovy (or Vice Versa)

The rule: 'This project uses Gradle Kotlin DSL (.gradle.kts files). Never generate Groovy DSL (.gradle files). Use typed accessors for plugins and extensions. Use string-based dependency notation with version catalogs: implementation(libs.spring.boot.starter). Use Kotlin syntax: val instead of def, fun instead of method definitions.'

For Groovy DSL projects: 'This project uses Gradle Groovy DSL (.gradle files). Never generate .kts files. Use Groovy closure syntax for configuration blocks. Use single-quoted strings for non-interpolated values, double-quoted for interpolated. Use the plugins block (not apply plugin:) for plugin application.'

The DSL choice must be declared — AI mixes them constantly. A .gradle.kts file with Groovy syntax doesn't compile. A .gradle file with Kotlin syntax doesn't compile. One rule prevents every DSL mismatch error.

⚠️ DSL Mismatch = Won't Compile

Groovy syntax in .gradle.kts doesn't compile. Kotlin syntax in .gradle doesn't compile. Declare your DSL once: 'This project uses Gradle Kotlin DSL.' One rule prevents every build file syntax error.

Rule 2: Version Catalogs for Dependencies

The rule: 'Use Gradle version catalogs (libs.versions.toml) for all dependency declarations. Never hardcode version numbers in build files: implementation("org.springframework.boot:spring-boot-starter:3.2.0") becomes implementation(libs.spring.boot.starter). Define versions, libraries, bundles, and plugins in gradle/libs.versions.toml. Use bundles for groups of libraries that are always used together.'

For the TOML file: 'Organize versions in a [versions] section, libraries in [libraries] section, and plugins in [plugins] section. Use aliases that match the Maven artifact name: spring-boot-starter for org.springframework.boot:spring-boot-starter. Pin versions to exact releases — no dynamic versions (+, latest.release) in production builds.'

AI assistants hardcode versions in every build.gradle because that's what most online examples show. Version catalogs centralize all versions in one file — changing a version affects every subproject, and the IDE provides type-safe accessors.

  • gradle/libs.versions.toml for all dependency versions
  • implementation(libs.x.y) over implementation("group:artifact:version")
  • Bundles for library groups: libs.bundles.testing = [junit, mockito, assertj]
  • No dynamic versions (+, latest.release) — pin exact versions
  • Renovate/Dependabot updates the TOML file — not scattered build files
💡 One File for All Versions

Version catalogs (libs.versions.toml) centralize every dependency version. Changing a version updates every subproject. The IDE provides type-safe accessors. No more grepping build files for version strings.

Rule 3: Convention Plugins for Multi-Project Builds

The rule: 'For multi-project builds, use convention plugins in buildSrc/ or an included build (build-logic/). Convention plugins apply shared configuration: Java version, testing framework, code quality tools, publishing config. Never duplicate configuration across subproject build files — extract to a convention plugin and apply it.'

For common conventions: 'Create plugins like: my-java-library (Java version + testing + publishing), my-spring-boot (Spring Boot + actuator + testing), my-kotlin-library (Kotlin + coroutines + testing). Each subproject's build file applies the relevant convention and adds only project-specific dependencies.'

Convention plugins turn a 50-line build file per subproject into a 5-line build file. The AI doesn't know about your convention plugins unless you tell it — this rule prevents it from regenerating shared configuration in every subproject.

Rule 4: Configuration Avoidance and Performance

The rule: 'Use configuration avoidance APIs: tasks.register instead of tasks.create, tasks.named instead of tasks.getByName. Never use the eager API — it configures every task on every build, even tasks that won't execute. Use the build cache: org.gradle.caching=true in gradle.properties. Use configuration cache for faster configuration phase: org.gradle.configuration-cache=true.'

For build speed: 'Enable parallel execution: org.gradle.parallel=true. Use the Gradle daemon: org.gradle.daemon=true (default). Set appropriate JVM heap: org.gradle.jvmargs=-Xmx2g. Use incremental compilation. Profile slow builds with --scan to identify bottlenecks.'

AI assistants use the eager API (tasks.create, project.tasks.getByName) because it's simpler. The eager API configures all tasks on every invocation — even ./gradlew help configures your entire test and release pipeline. Configuration avoidance can cut configuration time by 50-80%.

ℹ️ 50-80% Faster Config

tasks.register (lazy) vs tasks.create (eager) — the lazy API skips configuring tasks that won't execute. Even `./gradlew help` configures your entire pipeline with the eager API. Avoidance cuts config time dramatically.

Rule 5: Correct Dependency Configurations

The rule: 'Use the correct dependency configuration: implementation for internal dependencies (not exposed to consumers), api for dependencies that leak through your API, compileOnly for compile-time-only (Lombok, annotation processors), runtimeOnly for runtime-only (JDBC drivers), testImplementation for test dependencies. Never use deprecated configurations: compile, runtime, testCompile, testRuntime.'

For platform dependencies: 'Use platform() for BOM imports: implementation(platform(libs.spring.boot.bom)). Use enforcedPlatform() only when you need to override transitive versions. Use constraints block for version alignment without importing a BOM.'

The implementation vs api distinction matters for build performance and dependency leaking. implementation keeps dependencies off the consumer's compile classpath — faster compilation and cleaner dependency graphs. AI defaults to implementation (correct for most cases) but sometimes uses api when implementation would suffice.

  • implementation: internal — doesn't leak to consumers' compile classpath
  • api: leaks to consumers — only for types used in your public API
  • compileOnly: compile-time only — Lombok, annotation processors
  • runtimeOnly: runtime only — JDBC drivers, logging backends
  • testImplementation: test dependencies — JUnit, Mockito, AssertJ
  • Never: compile, runtime, testCompile, testRuntime (all deprecated)

Complete Gradle Rules Template

Consolidated rules for Gradle projects. Specify your DSL (Kotlin or Groovy) in the first line.

  • Declare DSL: Kotlin (.kts) or Groovy (.gradle) — AI must generate the correct one
  • Version catalogs: gradle/libs.versions.toml — no hardcoded versions in build files
  • Convention plugins in buildSrc/ or build-logic/ — no duplicated config across subprojects
  • Configuration avoidance: tasks.register over tasks.create, tasks.named over getByName
  • Build cache + configuration cache + parallel + daemon enabled in gradle.properties
  • implementation over api unless type leaks — never deprecated compile/runtime
  • platform() for BOMs — enforcedPlatform() only when overriding transitives
  • Gradle wrapper committed — pin Gradle version — renovate for dependency updates