Gradle Coding Standards (Gradle 9 LTS)

SkillProductivity

Gradle build tool standards focusing on Kotlin DSL. Covers project configuration, dependency management, and custom plugin/task development with Gradle 9 LTS.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Gradle Coding Standards (Gradle 9 LTS) skill

What this skill tells your AI

The instructions your AI receives, as published by b33eep/claude-code-setup in skills/standards-gradle/SKILL.md and read by ahel’s review.

This skill provides comprehensive guidance for Gradle build configuration using Kotlin DSL (.gradle.kts). It covers both everyday project configuration and advanced plugin/task development patterns based on Gradle 9 LTS.

Core Principles

  1. Declarative over Imperative: Prefer declarative configuration that describes what you want, not how to achieve it
  2. Type-Safe Configuration: Use Kotlin DSL for type safety and IDE support
  3. Lazy Configuration: Use Providers API to defer configuration until needed
  4. Build Cache Friendly: Write tasks that support build caching for faster builds
  5. Configuration Cache Compatible: Ensure build scripts work with configuration cache for optimal performance

Section 1: Project Configuration

This section covers the common scenarios developers encounter when configuring Gradle projects: setting up build scripts, managing dependencies, applying plugins, and structuring multi-module projects.

Build Script Basics

build.gradle.kts Structure

Organize your build script in a consistent, readable order:

// 1. Plugin declarations (always first)
plugins {
    java
    application
    id("com.github.johnrengelman.shadow") version "8.1.1"
}

// 2. Project properties and versioning
group = "com.example"
version = "1.0.0"

// 3. Repositories
repositories {
    mavenCentral()
}

// 4. Dependencies
dependencies {
    implementation("com.google.guava:guava:33.0.0-jre")
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
}

// 5. Java/Kotlin configuration
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

// 6. Task configuration
tasks {
    test {
        useJUnitPlatform()
    }

    jar {
        manifest {
            attributes("Main-Class" to "com.example.Main")
        }
    }
}
settings.gradle.kts Basics
// Root project name
rootProject.name = "my-project"

// Enable Gradle version catalogs (Gradle 9+)
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")

// Include subprojects
include("app")
include("lib")
include("common")

// Optional: Customize subproject location
project(":app").projectDir = file("applications/app")
Repository Configuration
repositories {
    // GOOD: Standard repositories first
    mavenCentral()

    // GOOD: Google repository for Android/Google libraries
    google()

    // GOOD: Custom repository with HTTPS
    maven {
        name = "CompanyRepo"
        url = uri("https://repo.company.com/maven")
        credentials {
            username = providers.gradleProperty("repoUser").orNull
            password = providers.gradleProperty("repoPassword").orNull
        }
    }
}

// BAD: Using HTTP instead of HTTPS (security risk)
// maven { url = uri("http://insecure-repo.com/maven") }

// BAD: Exposing credentials in build script
// maven {
//     url = uri("https://repo.company.com/maven")
//     credentials {
//         username = "hardcoded-user"  // Never do this!
//         password = "hardcoded-pass"  // Never do this!
//     }
// }
Script Organization Best Practices
// GOOD: Use extra properties for shared values
val mockitoVersion by extra("5.10.0")
val junitVersion by extra("5.10.2")

dependencies {
    testImplementation("org.mockito:mockito-core:$mockitoVersion")
    testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion")
}

// GOOD: Extract complex configuration to functions
fun configureJavaToolchain() {
    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(21)
            vendor = JvmVendorSpec.ADOPTIUM
        }
    }
}

// Apply configuration
configureJavaToolchain()

Gradle Build Phases

Understanding Gradle's build phases is essential for writing efficient build scripts and understanding when your code executes.

The Three Build Phases

Every Gradle build runs through three distinct phases in order:

  1. Initialization Phase - Determines which projects participate in the build
  2. Configuration Phase - Configures all projects and builds the task graph
  3. Execution Phase - Executes the selected tasks

Understanding these phases helps you:

  • Write faster builds (keep configuration phase light)
  • Understand lazy evaluation and Provider API
  • Make configuration cache work correctly
  • Debug build script behavior
1. Initialization Phase

Purpose: Determine project structure and which projects participate in the build.

What runs: settings.gradle.kts files

What happens:

  • Gradle locates and reads settings.gradle.kts
  • Determines root project and subprojects
  • Creates Project instances for each project

Example:

// settings.gradle.kts (runs during initialization)
rootProject.name = "my-project"

println("Initialization phase")  // Prints during initialization

include("app")
include("lib")
include("common")

// Optional: Customize subproject directories
project(":app").projectDir = file("applications/app")

Duration: Very fast (typically < 100ms)

Key Point: You cannot access Project objects yet - they're being created.

2. Configuration Phase

Purpose: Configure all tasks and build the task execution graph.

What runs: All build.gradle.kts files for participating projects

What happens:

  • Applies plugins
  • Evaluates all top-level code in build scripts
  • Configures tasks (but doesn't execute them)
  • Builds task dependency graph
  • Prepares for execution

Example:

// build.gradle.kts (runs during configuration)

plugins {
    java  // Runs during configuration
}

version = "1.0.0"  // Runs during configuration

println("Configuration phase")  // Runs during configuration

tasks.register("myTask") {
    group = "custom"  // Runs during configuration
    description = "Example task"  // Runs during configuration

    println("Task configuration")  // Runs during configuration

    doLast {
        println("Task execution")  // Does NOT run during configuration!
    }
}

// This runs during configuration
val projectVersion = version
println("Project version: $projectVersion")

// BAD: Expensive work during configuration
// val allFiles = File("src").walkTopDown().toList()  // Slows every build!

// GOOD: Use providers for lazy evaluation
val sourceFiles: Provider<FileTree> = providers.provider {
    fileTree("src")  // Only evaluated when needed
}

Duration: Can be slow if not careful (seconds to minutes for large projects)

Key Point: Configuration runs on every build, even if no tasks execute. Keep it fast!

3. Execution Phase

Purpose: Execute the selected tasks in dependency order.

What runs: Task actions (doFirst, doLast, @TaskAction)

What happens:

  • Tasks execute in correct dependency order
  • Task inputs are read
  • Task outputs are generated
  • Build artifacts are created

Example:

tasks.register("myTask") {
    // Configuration phase
    group = "custom"

    doFirst {
        // Execution phase - runs first
        println("Starting task")
    }

    doLast {
        // Execution phase - runs last
        println("Task completed")
    }
}

// Abstract task with @TaskAction
abstract class BuildTask : DefaultTask() {
    @get:InputDirectory
    abstract val sourceDir: DirectoryProperty

    @get:OutputDirectory
    abstract val outputDir: DirectoryProperty

    @TaskAction  // Execution phase
    fun build() {
        println("Building...")
        // Actual work happens here
    }
}

Duration: Depends on what tasks do (compile, test, package, etc.)

Key Point: Only requested tasks (and their dependencies) execute.

When Code Runs - Quick Reference
Code LocationPhaseExample
settings.gradle.kts (top-level)InitializationrootProject.name = "app"
build.gradle.kts (top-level)Configurationversion = "1.0"
plugins {} blockConfigurationjava
dependencies {} blockConfigurationimplementation(...)
tasks.register { } outer blockConfigurationgroup = "custom"
tasks.register { } inner blockConfigurationdependsOn("other")
Extension configuration blocksConfigurationjava { toolchain { } }
doFirst { }Executionprintln("starting")
doLast { }Executionprintln("done")
@TaskAction methodExecutionfun execute() { }
Provider.get() in doLastExecutionval v = provider.get()
Common Mistakes and Anti-Patterns
// ❌ BAD: Expensive I/O during configuration
tasks.register("badTask") {
    val files = File("src").listFiles()  // I/O during configuration - runs every build!
    println("Found ${files?.size} files")

    doLast {
        println("Processing ${files?.size} files")
    }
}

// ✅ GOOD: Defer work to execution
tasks.register("goodTask") {
    doLast {
        val files = File("src").listFiles()  // I/O during execution - only when task runs
        println("Found ${files?.size} files")
        println("Processing ${files.size} files")
    }
}

// ❌ BAD: Accessing task outputs during configuration
tasks.register("badConsumer") {
    val compileOutput = tasks.named("compileJava").get().outputs.files  // Not ready yet!

    doLast {
        println(compileOutput)
    }
}

// ✅ GOOD: Use providers to defer access
tasks.register("goodConsumer") {
    val compileOutput = tasks.named("compileJava").map { it.outputs.files }

    doLast {
        println(compileOutput.get())  // Resolved during execution
    }
}

// ❌ BAD: Network calls during configuration
tasks.register("badFetch") {
    val response = URL("https://api.example.com/version").readText()  // Slows every build!

    doLast {
        println("Version: $response")
    }
}

// ✅ GOOD: Use providers for network calls
tasks.register("goodFetch") {
    val response: Provider<String> = providers.provider {
        URL("https://api.example.com/version").readText()
    }

    doLast {
        println("Version: ${response.get()}")  // Only called during execution
    }
}

// ❌ BAD: Calling .get() on providers during configuration
tasks.register("badProvider") {
    val version = providers.gradleProperty("version").get()  // Eager evaluation!

    doLast {
        println("Version: $version")
    }
}

// ✅ GOOD: Defer .get() until execution
tasks.register("goodProvider") {
    val version = providers.gradleProperty("version")  // Lazy - not evaluated yet

    doLast {
        println("Version: ${version.get()}")  // Evaluated here
    }
}

// ❌ BAD: Mutating shared state during configuration
var counter = 0  // Global mutable state

tasks.register("bad1") {
    counter++  // Modifies global state during configuration
    doLast { println("Counter: $counter") }
}

tasks.register("bad2") {
    counter++  // Order-dependent!
    doLast { println("Counter: $counter") }
}

// ✅ GOOD: Use build services or task outputs for shared state
Why Build Phases Matter

1. Build Performance

Configuration phase runs on every build:

./gradlew tasks       # Configuration runs
./gradlew clean       # Configuration runs
./gradlew build       # Configuration runs
./gradlew --stop      # Configuration runs

Slow configuration = slow every command, even ./gradlew tasks!

2. Configuration Cache

Configuration cache stores the result of configuration phase:

# First run: Configuration + execution
./gradlew build --configuration-cache
# Configuration phase: 5 seconds
# Execution phase: 30 seconds

# Second run: Execution only
./gradlew clean build --configuration-cache
# Configuration phase: 0 seconds (reused from cache!)
# Execution phase: 30 seconds

Benefits:

  • Up to 90% faster builds (skip configuration entirely)
  • Especially valuable for large projects

Requirements:

  • Use Provider API (lazy evaluation)
  • No mutable shared state
  • No accessing project during execution
  • Serializable configuration

3. Up-to-Date Checks

Tasks are up-to-date when:

  • Inputs haven't changed
  • Outputs exist and are valid

Input/output annotations are evaluated during:

  • Configuration: Gradle determines task inputs/outputs
  • Execution: Gradle checks if task needs to run

Proper annotations enable:

  • Incremental builds
  • Build cache
  • FROM-CACHE and UP-TO-DATE optimizations
Best Practices for Build Phases

Do:

  • ✅ Keep configuration phase fast (< 1 second per project ideal)
  • ✅ Use tasks.register() for lazy task creation
  • ✅ Use Provider API for lazy evaluation
  • ✅ Defer expensive work to execution phase
  • ✅ Use @Input/@Output annotations properly
  • ✅ Test with --configuration-cache to catch issues

Don't:

  • ❌ Perform I/O during configuration (file scanning, network calls)
  • ❌ Use tasks.create() (eager - prefer register())
  • ❌ Call .get() on providers during configuration
  • ❌ Access task outputs during configuration
  • ❌ Mutate global/shared state during configuration
  • ❌ Use project references in task actions
Debugging Build Phases
# See configuration time breakdown
./gradlew build --profile
# Open: build/reports/profile/profile-<timestamp>.html

# Measure configuration time
./gradlew build --configuration-cache --configuration-cache-problems=warn

# See what runs during configuration
./gradlew build --info | grep "Configuration"

# Test configuration cache compatibility
./gradlew build --configuration-cache
./gradlew clean build --configuration-cache  # Should show "Reusing configuration cache"
Example: Full Build Lifecycle
// settings.gradle.kts
println("1. Initialization phase: settings.gradle.kts")
rootProject.name = "lifecycle-demo"

// build.gradle.kts
println("2. Configuration phase: build.gradle.kts top-level")

plugins {
    java
    println("3. Configuration phase: plugins block")
}

println("4. Configuration phase: after plugins")

tasks.register("demo") {
    println("5. Configuration phase: task configuration")

    group = "demo"
    description = "Demonstrates build phases"

    doFirst {
        println("7. Execution phase: doFirst")
    }

    doLast {
        println("8. Execution phase: doLast")
    }
}

println("6. Configuration phase: after task registration")

// When you run: ./gradlew demo
// Output order:
// 1. Initialization phase: settings.gradle.kts
// 2. Configuration phase: build.gradle.kts top-level
// 3. Configuration phase: plugins block
// 4. Configuration phase: after plugins
// 5. Configuration phase: task configuration
// 6. Configuration phase: after task registration
// 7. Execution phase: doFirst
// 8. Execution phase: doLast

Dependency Management

Dependency Configurations
dependencies {
    // GOOD: implementation - for internal dependencies (not exposed to consumers)
    implementation("com.google.guava:guava:33.0.0-jre")

    // GOOD: api - for dependencies exposed to consumers (libraries only)
    // Only available with java-library plugin
    api("org.apache.commons:commons-lang3:3.14.0")

    // GOOD: compileOnly - compile-time only (not packaged)
    compileOnly("org.projectlombok:lombok:1.18.30")

    // GOOD: runtimeOnly - runtime only (not on compile classpath)
    runtimeOnly("com.h2database:h2:2.2.224")

    // GOOD: testImplementation - for test code only
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
    testImplementation("org.mockito:mockito-core:5.10.0")

    // GOOD: testRuntimeOnly - test runtime only
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

// BAD: Using 'compile' (deprecated in Gradle 7+)
// dependencies {
//     compile("some:library:1.0")  // Use 'implementation' instead
// }

// BAD: Using 'runtime' (deprecated in Gradle 7+)
// dependencies {
//     runtime("some:library:1.0")  // Use 'runtimeOnly' instead
// }
Version Catalogs (Modern Gradle Approach)

gradle/libs.versions.toml:

[versions]
guava = "33.0.0-jre"
junit = "5.10.2"
mockito = "5.10.0"
kotlin = "2.0.0"

[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" }
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }

[bundles]
testing = ["junit-jupiter", "mockito-core"]

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
shadow = { id = "com.github.johnrengelman.shadow", version = "8.1.1" }

build.gradle.kts:

plugins {
    alias(libs.plugins.kotlin.jvm)
}

dependencies {
    // GOOD: Type-safe accessors from version catalog
    implementation(libs.guava)
    testImplementation(libs.bundles.testing)
    testRuntimeOnly(libs.junit.platform.launcher)
}

// Benefits:
// - Centralized version management
// - Type-safe accessors with IDE completion
// - Easy to share across multi-module projects
// - Prevents version conflicts
Dependency Constraints
dependencies {
    implementation("com.example:library:1.0")

    // GOOD: Force specific version to resolve conflicts
    constraints {
        implementation("org.slf4j:slf4j-api:2.0.9") {
            because("Earlier versions have security vulnerabilities")
        }
    }

    // GOOD: Align versions across dependency group
    constraints {
        implementation("org.springframework.boot:spring-boot-starter-web:3.2.0")
        implementation("org.springframework.boot:spring-boot-starter-data-jpa:3.2.0")
    }
}
Platform/BOM Dependencies
dependencies {
    // GOOD: Import BOM (Bill of Materials) for version alignment
    implementation(platform("org.springframework.boot:spring-boot-dependencies:3.2.0"))

    // Now you can omit versions - they come from the BOM
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")

    // GOOD: For testing, use testImplementation(platform(...))
    testImplementation(platform("org.junit:junit-bom:5.10.2"))
    testImplementation("org.junit.jupiter:junit-jupiter")
}
Excluding Transitive Dependencies
dependencies {
    // GOOD: Exclude specific transitive dependency
    implementation("com.example:library:1.0") {
        exclude(group = "commons-logging", module = "commons-logging")
    }

    // GOOD: Exclude all transitive dependencies (rare case)
    implementation("com.example:utility:1.0") {
        isTransitive = false
    }

    // Replace excluded dependency with alternative
    implementation("org.slf4j:jcl-over-slf4j:2.0.9")
}

// GOOD: Exclude globally (affects all dependencies)
configurations.all {
    exclude(group = "commons-logging", module = "commons-logging")
}
Dependency Notation
dependencies {
    // GOOD: String notation (most common)
    implementation("com.google.guava:guava:33.0.0-jre")

    // GOOD: Map notation (when you need more control)
    implementation(group = "com.google.guava", name = "guava", version = "33.0.0-jre")

    // GOOD: With classifier
    implementation("net.java.dev.jna:jna:5.13.0:jpms")

    // GOOD: Local file dependency
    implementation(files("libs/custom-library.jar"))

    // GOOD: File tree dependency
    implementation(fileTree("libs") { include("*.jar") })

    // GOOD: Project dependency (multi-module)
    implementation(project(":common"))
}

Plugin Configuration

Plugin Application
plugins {
    // GOOD: Core plugins (no version needed)
    java
    application

    // GOOD: External plugin with version
    id("com.github.johnrengelman.shadow") version "8.1.1"

    // GOOD: Kotlin plugin
    kotlin("jvm") version "2.0.0"

    // GOOD: Apply false (for root project in multi-module)
    id("org.springframework.boot") version "3.2.0" apply false
}

// BAD: Old apply() syntax (avoid in new code)
// apply(plugin = "java")  // Use plugins {} block instead
Using Version Catalogs with Plugins
// gradle/libs.versions.toml
// [plugins]
// kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version = "2.0.0" }
// shadow = { id = "com.github.johnrengelman.shadow", version = "8.1.1" }

plugins {
    // GOOD: Type-safe plugin declaration from catalog
    alias(libs.plugins.kotlin.jvm)
    alias(libs.plugins.shadow)
}
Common Plugins

Java Plugin:

plugins {
    java
}

java {
    // GOOD: Use Java toolchain (modern approach)
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
        vendor = JvmVendorSpec.ADOPTIUM
    }

    // GOOD: Configure compatibility (legacy approach)
    // sourceCompatibility = JavaVersion.VERSION_21
    // targetCompatibility = JavaVersion.VERSION_21

    // GOOD: Enable automatic module name for JPMS
    modularity.inferModulePath = true

    // GOOD: Generate sources and javadoc JARs
    withSourcesJar()
    withJavadocJar()
}

Kotlin JVM Plugin:

plugins {
    kotlin("jvm") version "2.0.0"
}

kotlin {
    // GOOD: Set JVM target
    jvmToolchain(21)

    // GOOD: Enable explicit API mode (libraries)
    explicitApi()

    // GOOD: Compiler options
    compilerOptions {
        freeCompilerArgs.add("-Xjsr305=strict")
        allWarningsAsErrors = true
    }
}

Application Plugin:

plugins {
    application
}

application {
    // GOOD: Set main class
    mainClass = "com.example.Main"

    // GOOD: Configure application name
    applicationName = "my-app"

    // GOOD: Set default JVM args
    applicationDefaultJvmArgs = listOf("-Xmx512m", "-Xms256m")
}

// Run with: ./gradlew run
// Package with: ./gradlew installDist

Java Library Plugin:

plugins {
    `java-library`  // Note the backticks for kebab-case
}

dependencies {
    // GOOD: Use 'api' for exposed dependencies
    api("org.apache.commons:commons-lang3:3.14.0")

    // GOOD: Use 'implementation' for internal dependencies
    implementation("com.google.guava:guava:33.0.0-jre")
}

// Consumers of this library get:
// - api dependencies on their compile classpath
// - implementation dependencies are hidden
Configuring Plugin Extensions
plugins {
    java
    jacoco
}

// GOOD: Configure extension in dedicated block
jacoco {
    toolVersion = "0.8.11"
    reportsDirectory = layout.buildDirectory.dir("reports/jacoco")
}

// GOOD: Configure task created by plugin
tasks.jacocoTestReport {
    dependsOn(tasks.test)
    reports {
        xml.required = true
        html.required = true
        csv.required = false
    }
}

// BAD: Accessing extension before plugin is applied
// jacoco { ... }  // Will fail if jacoco plugin not applied
// plugins { jacoco }  // Plugin should come first
Conditional Plugin Application
plugins {
    java
    if (project.hasProperty("enableKotlin")) {
        kotlin("jvm") version "2.0.0"
    }
}

// Alternative: Apply plugin conditionally
if (project.findProperty("coverage") == "true") {
    apply(plugin = "jacoco")
}

Multi-Module Projects

Project Structure
my-project/
├── settings.gradle.kts         # Project structure definition
├── build.gradle.kts            # Root build script
├── gradle/
│   └── libs.versions.toml      # Shared version catalog
├── app/
│   ├── build.gradle.kts        # Application module
│   └── src/
├── lib/
│   ├── build.gradle.kts        # Library module
│   └── src/
└── common/
    ├── build.gradle.kts        # Shared code module
    └── src/

settings.gradle.kts:

rootProject.name = "my-project"

// Enable type-safe project accessors (Gradle 7+)
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")

include("app")
include("lib")
include("common")

// Optional: Nested modules
include("backend:api")
include("backend:service")

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
56
Forks
6
Last commit
May 2026
Advanced
Catalog kind
skill
Gateway key
standards-gradle
Source
github.com/b33eep/claude-code-setup