@b33eep/standards-gradle
skillGradle build tool standards focusing on Kotlin DSL. Covers project configuration, dependency management, and custom plugin/task development with Gradle 9 LTS.
apm::install
apm install @b33eep/standards-gradleapm::skill.md
---
name: standards-gradle
description: Gradle build tool standards focusing on Kotlin DSL. Covers project configuration, dependency management, and custom plugin/task development with Gradle 9 LTS.
type: context
applies_to: [gradle]
file_extensions: [".gradle.kts", ".gradle"]
---
# Gradle Coding Standards (Gradle 9 LTS)
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:
```kotlin
// 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
```kotlin
// 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
```kotlin
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
```kotlin
// 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:**
```kotlin
// 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:**
```kotlin
// 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:**
```kotlin
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 Location | Phase | Example |
|---------------|-------|---------|
| `settings.gradle.kts` (top-level) | Initialization | `rootProject.name = "app"` |
| `build.gradle.kts` (top-level) | Configuration | `version = "1.0"` |
| `plugins {}` block | Configuration | `java` |
| `dependencies {}` block | Configuration | `implementation(...)` |
| `tasks.register { }` outer block | Configuration | `group = "custom"` |
| `tasks.register { }` inner block | Configuration | `dependsOn("other")` |
| Extension configuration blocks | Configuration | `java { toolchain { } }` |
| `doFirst { }` | Execution | `println("starting")` |
| `doLast { }` | Execution | `println("done")` |
| `@TaskAction` method | Execution | `fun execute() { }` |
| Provider.get() in doLast | Execution | `val v = provider.get()` |
#### Common Mistakes and Anti-Patterns
```kotlin
// ❌ 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:
```bash
./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:
```bash
# 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
```bash
# 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
```kotlin
// 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
```kotlin
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:**
```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:**
```kotlin
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
```kotlin
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
```kotlin
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
```kotlin
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
```kotlin
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
```kotlin
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
```kotlin
// 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:**
```kotlin
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:**
```kotlin
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:**
```kotlin
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:**
```kotlin
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
```kotlin
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
```kotlin
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:**
```kotlin
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")
```
**Root build.gradle.kts:**
```kotlin
plugins {
// GOOD: Apply plugins to all subprojects
java apply false
kotlin("jvm") version "2.0.0" apply false
}
// GOOD: Configure all projects (including root)
allprojects {
group = "com.example"
version = "1.0.0"
repositories {
mavenCentral()
}
}
// GOOD: Configure only subprojects
subprojects {
// Apply common configuration here
}
```
#### Convention Plugins (Recommended Approach)
Convention plugins encapsulate shared configuration in a type-safe, reusable way.
**buildSrc/build.gradle.kts:**
```kotlin
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
}
```
**buildSrc/src/main/kotlin/java-conventions.gradle.kts:**
```kotlin
plugins {
java
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.test {
useJUnitPlatform()
}
```
**app/build.gradle.kts:**
```kotlin
plugins {
id("java-conventions") // Apply convention plugin
application
}
application {
mainClass = "com.example.app.Main"
}
dependencies {
implementation(project(":lib"))
implementation(project(":common"))
}
```
**lib/build.gradle.kts:**
```kotlin
plugins {
id("java-conventions") // Apply convention plugin
`java-library`
}
dependencies {
api(project(":common"))
implementation("com.google.guava:guava:33.0.0-jre")
}
```
#### Cross-Module Dependencies
```kotlin
dependencies {
// GOOD: Type-safe project accessor (with TYPESAFE_PROJECT_ACCESSORS)
implementation(projects.common)
implementation(projects.backend.api)
// GOOD: String-based (works without feature preview)
implementation(project(":common"))
implementation(project(":backend:api"))
// GOOD: Depend on specific configuration
testImplementation(project(path = ":lib", configuration = "testFixtures"))
}
```
#### Shared Configuration Patterns
**Pattern 1: subprojects {} (Quick but limited)**
```kotlin
subprojects {
apply(plugin = "java")
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
}
}
// BAD: Hard to override, not type-safe, mixes concerns
```
**Pattern 2: Convention Plugins (Recommended)**
```kotlin
// buildSrc/src/main/kotlin/java-library-conventions.gradle.kts
plugins {
`java-library`
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
// GOOD: Type-safe, reusable, easy to override
// Modules apply with: plugins { id("java-library-conventions") }
```
#### buildSrc vs Included Builds vs Composite Builds
**buildSrc (For Convention Plugins):**
- Built automatically before main build
- Used for convention plugins and build logic
- Not published
- Changes require Gradle daemon restart
```
project/
├── buildSrc/
│ ├── build.gradle.kts
│ └── src/main/kotlin/
│ └── java-conventions.gradle.kts
└── build.gradle.kts
```
**Included Builds (For Build Logic Libraries):**
- Separate Gradle project included in your build
- Can be published independently
- Changes don't require daemon restart
**settings.gradle.kts:**
```kotlin
includeBuild("build-logic")
include("app")
include("lib")
```
**Composite Builds (For Multi-Repo Projects):**
- Combine multiple independent Gradle builds
- Each build has its own settings.gradle.kts
**settings.gradle.kts:**
```kotlin
includeBuild("../other-project")
```
#### Dependency Management Across Modules
**Using Version Catalogs (Recommended):**
```kotlin
// gradle/libs.versions.toml (at root)
[versions]
guava = "33.0.0-jre"
[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
// All modules can use: implementation(libs.guava)
```
**Platform Projects (Alternative):**
```kotlin
// platform/build.gradle.kts
plugins {
`java-platform`
}
dependencies {
constraints {
api("com.google.guava:guava:33.0.0-jre")
api("org.slf4j:slf4j-api:2.0.9")
}
}
// Other modules:
dependencies {
implementation(platform(project(":platform")))
implementation("com.google.guava:guava") // Version from platform
}
```
### Gradle 9 Features
Gradle 9 is the latest LTS (Long-Term Support) release with significant improvements to performance, developer experience, and build reliability.
#### Configuration Cache (Stable in Gradle 9)
Configuration cache dramatically speeds up builds by caching the result of the configuration phase.
**Enable in gradle.properties:**
```properties
org.gradle.configuration-cache=true
```
**Or via command line:**
```bash
./gradlew build --configuration-cache
```
**Benefits:**
- Up to 90% faster for configuration-heavy builds
- Second builds reuse cached configuration
- Encourages better build practices
**Making Your Build Compatible:**
```kotlin
// GOOD: Use providers instead of direct property access
val myProperty: Provider<String> = providers.gradleProperty("myProp")
tasks.register("example") {
doLast {
println(myProperty.get()) // Lazy evaluation
}
}
// BAD: Direct property access (breaks configuration cache)
// val value = project.findProperty("myProp") // Evaluated at configuration time
```
#### Build Cache (Enhanced in Gradle 9)
**Enable in gradle.properties:**
```properties
org.gradle.caching=true
```
**Or via command line:**
```bash
./gradlew build --build-cache
```
**Configure cache:**
```kotlin
buildCache {
local {
isEnabled = true
directory = file("${rootDir}/.gradle/build-cache")
removeUnusedEntriesAfterDays = 30
}
remote<HttpBuildCache> {
isEnabled = true
url = uri("https://cache.example.com/")
isPush = System.getenv("CI") == "true" // Only push from CI
credentials {
username = providers.gradleProperty("cacheUser").orNull
password = providers.gradleProperty("cachePassword").orNull
}
}
}
```
#### Improved Test Suites API
```kotlin
testing {
suites {
val test by getting(JvmTestSuite::class) {
useJUnitJupiter("5.10.2")
}
// GOOD: Define integration test suite
val integrationTest by registering(JvmTestSuite::class) {
testType = TestSuiteType.INTEGRATION_TEST
dependencies {
implementation(project())
implementation("org.testcontainers:junit-jupiter:1.19.3")
}
targets {
all {
testTask.configure {
shouldRunAfter(test)
}
}
}
}
}
}
// Run with: ./gradlew integrationTest
```
#### Java Toolchains (Enhanced)
```kotlin
java {
toolchain {
// GOOD: Specify vendor
vendor = JvmVendorSpec.ADOPTIUM
languageVersion = JavaLanguageVersion.of(21)
// GOOD: Gradle auto-downloads if not available
}
}
// GOOD: Use different toolchain for specific task
tasks.register<JavaExec>("runWithJava17") {
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(17)
}
}
```
#### Problems API (New in Gradle 8+, Refined in 9)
Better error reporting and problem aggregation:
```kotlin
// Gradle automatically collects and reports problems
// Your build output now shows:
// - Aggregated problems
// - Actionable error messages
// - Problem locations with file:line references
// No configuration needed - it just works better!
```
#### Isolated Projects (Experimental in Gradle 9)
Parallel configuration of subprojects for massive multi-module builds.
```properties
# gradle.properties
org.gradle.unsafe.isolated-projects=true
```
**Benefits:**
- Parallel configuration of independent projects
- Reduced configuration time for large builds
- Requires strict project isolation
#### Deprecated Features to Avoid
```kotlin
// BAD: compile, runtime configurations (removed in Gradle 8+)
// dependencies {
// compile("some:library:1.0") // Use 'implementation'
// runtime("some:library:1.0") // Use 'runtimeOnly'
// }
// BAD: Old task creation API (prefer register)
// tasks.create("myTask") { ... } // Use tasks.register("myTask") { ... }
// BAD: Convention properties (use extensions)
// project.convention.plugins // Use project.extensions
// BAD: Direct task execution during configuration
// tasks.named("build").get().execute() // Never execute tasks during configuration
```
#### Performance Improvements in Gradle 9
1. **Faster dependency resolution** - Up to 40% faster for large dependency graphs
2. **Improved incremental compilation** - Better change detection for Java/Kotlin
3. **Enhanced file system watching** - More efficient change detection
4. **Better daemon memory management** - Reduced memory usage over time
5. **Optimized configuration cache** - Faster serialization/deserialization
#### Best Practices for Gradle 9
```kotlin
// GOOD: Enable all performance features
// gradle.properties:
org.gradle.caching=true
org.gradle.configuration-cache=true
org.gradle.parallel=true
org.gradle.vfs.watch=true
// GOOD: Use Java toolchains instead of sourceCompatibility
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
// GOOD: Use lazy task registration
tasks.register("myTask") {
doLast { ... }
}
// GOOD: Use Provider API for task inputs
abstract class MyTask : DefaultTask() {
@get:Input
abstract val message: Property<String>
@TaskAction
fun execute() {
println(message.get())
}
}
```
---
## Section 2: Plugin/Task Development
This section covers advanced topics for developers building custom Gradle plugins and tasks: proper input/output handling, extensions, lazy configuration with Providers API, and build caching.
### Custom Tasks
#### Lazy vs Eager Task Registration
```kotlin
// BAD: Eager task creation (always executed during configuration)
tasks.create("eagerTask") {
doLast {
println("Task executed")
}
}
// Problem: Task is configured immediately, slowing configuration phase
// GOOD: Lazy task registration (configured only when needed)
tasks.register("lazyTask") {
doLast {
println("Task executed")
}
}
// Benefit: Task configured only if needed (e.g., when explicitly run)
```
#### Task Actions
```kotlin
// GOOD: Simple task with doLast
tasks.register("hello") {
doLast {
println("Hello from task")
}
}
// GOOD: Multiple actions (executed in order)
tasks.register("multiAction") {
doFirst {
println("First action")
}
doLast {
println("Last action")
}
}
// GOOD: Named action (can be removed later if needed)
tasks.register("namedAction") {
val myAction = Action<Task> {
println("Named action")
}
doLast(myAction)
}
```
#### Abstract Task Classes (Recommended for Reusable Tasks)
```kotlin
import org.gradle.api.DefaultTask
import org.gradle.api.file.*
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
// GOOD: Abstract task with typed properties
abstract class ProcessFilesTask : DefaultTask() {
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val inputDir: DirectoryProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@get:Input
@get:Optional
abstract val prefix: Property<String>
init {
// Set defaults
prefix.convention("processed-")
}
@TaskAction
fun process() {
val input = inputDir.get().asFile
val output = outputDir.get().asFile
output.mkdirs()
input.listFiles()?.forEach { file ->
val processed = output.resolve("${prefix.get()}${file.name}")
processed.writeText(file.readText().uppercase())
}
println("Processed ${input.listFiles()?.size ?: 0} files")
}
}
// Register task with configuration
tasks.register<ProcessFilesTask>("processFiles") {
inputDir = layout.projectDirectory.dir("src/data")
outputDir = layout.buildDirectory.dir("processed")
prefix = "PROCESSED-"
}
```
#### Input and Output Annotations
**Critical for up-to-date checking and caching:**
```kotlin
abstract class AdvancedTask : DefaultTask() {
// GOOD: Input file
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE) // Content-only sensitivity
abstract val inputFile: RegularFileProperty
// GOOD: Input files
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE) // Path matters
abstract val inputFiles: ConfigurableFileCollection
// GOOD: Input directory
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val inputDir: DirectoryProperty
// GOOD: Input property (string, boolean, etc.)
@get:Input
abstract val message: Property<String>
// GOOD: Optional input
@get:Input
@get:Optional
abstract val optionalFlag: Property<Boolean>
// GOOD: Output file
@get:OutputFile
abstract val outputFile: RegularFileProperty
// GOOD: Output directory
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
// GOOD: Internal property (not an input/output)
@get:Internal
abstract val internalState: Property<String>
@TaskAction
fun execute() {
// Task implementation
}
}
```
**PathSensitivity options:**
- `NONE` - Only file content matters (not path or name)
- `NAME_ONLY` - File name matters
- `RELATIVE` - Relative path matters (most common)
- `ABSOLUTE` - Absolute path matters (rare)
#### Task Dependencies
```kotlin
// GOOD: Task depends on another task
tasks.register("taskA") {
doLast { println("Task A") }
}
tasks.register("taskB") {
dependsOn("taskA") // taskA runs before taskB
doLast { println("Task B") }
}
// GOOD: Multiple dependencies
tasks.register("taskC") {
dependsOn("taskA", "taskB")
doLast { println("Task C") }
}
// GOOD: Ordering without hard dependency
tasks.register("taskD") {
mustRunAfter("taskB") // If both run, D runs after B
doLast { println("Task D") }
}
tasks.register("taskE") {
shouldRunAfter("taskD") // Ordering hint (not enforced)
doLast { println("Task E") }
}
// GOOD: Finalization
tasks.register("taskF") {
doLast { println("Task F") }
}
tasks.register("cleanup") {
doLast { println("Cleanup") }
}
tasks.named("taskF") {
finalizedBy("cleanup") // cleanup always runs after taskF
}
```
#### Working Example: File Processing Task
```kotlin
abstract class TransformMarkdownTask : DefaultTask() {
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val markdownFiles: ConfigurableFileCollection
@get:OutputDirectory
abstract val htmlOutputDir: DirectoryProperty
@get:Input
abstract val title: Property<String>
init {
title.convention("Documentation")
}
@TaskAction
fun transform() {
val outputDir = htmlOutputDir.get().asFile
outputDir.mkdirs()
markdownFiles.forEach { mdFile ->
val htmlFile = outputDir.resolve("${mdFile.nameWithoutExtension}.html")
val content = mdFile.readText()
htmlFile.writeText("""
<!DOCTYPE html>
<html>
<head><title>${title.get()}</title></head>
<body>
<pre>$content</pre>
</body>
</html>
""".trimIndent())
}
logger.lifecycle("Transformed ${markdownFiles.files.size} markdown files")
}
}
// Register and configure
tasks.register<TransformMarkdownTask>("transformMarkdown") {
markdownFiles.from(fileTree("docs") { include("**/*.md") })
htmlOutputDir = layout.buildDirectory.dir("html")
title = "My Project Documentation"
}
```
#### Task Configuration Avoidance
```kotlin
// GOOD: Configure task only when needed
tasks.named<JavaCompile>("compileJava") {
options.compilerArgs.add("-Xlint:unchecked")
}
// BAD: Getting task eagerly (forces configuration)
// val compileJava = tasks.getByName("compileJava") // Avoid this
// GOOD: Lazy task reference
val compileJavaTask = tasks.named("compileJava")
// GOOD: Configure all tasks of type
tasks.withType<Test>().configureEach {
useJUnitPlatform()
maxParallelForks = Runtime.getRuntime().availableProcessors()
}
```
### Extension API
Extensions provide a DSL for configuring plugins. They're essential for creating user-friendly custom plugins.
#### Simple Extension
```kotlin
// Define extension (in buildSrc or custom plugin)
abstract class GreetingExtension {
abstract val message: Property<String>
abstract val times: Property<Int>
init {
// Set default values
message.convention("Hello")
times.convention(1)
}
}
// Register extension in plugin
class GreetingPlugin : Plugin<Project> {
override fun apply(project: Project) {
// Create extension
val extension = project.extensions.create("greeting", GreetingExtension::class.java)
// Use extension to configure task
project.tasks.register("greet") {
doLast {
repeat(extension.times.get()) {
println(extension.message.get())
}
}
}
}
}
// Usage in build.gradle.kts
plugins {
id("greeting-plugin")
}
greeting {
message = "Hello, Gradle!"
times = 3
}
```
#### Extension Anti-Patterns
```kotlin
// BAD: Using plain variables instead of Property<T>
abstract class BadExtension {
var message: String = "Hello" // Not lazy, not compatible with config cache
var times: Int = 1 // Cannot be wired to providers
}
// Problem: Breaks configuration cache, not lazy, no provider wiring
// GOOD: Always use Property<T>
abstract class GoodExtension {
abstract val message: Property<String>
abstract val times: Property<Int>
}
// BAD: Eager evaluation in extension
class BadPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create("bad", BadExtension::class.java)
// Evaluates immediately during configuration!
val msg = extension.message.get() // BAD: Too early
project.tasks.register("bad") {
doLast { println(msg) } // Value captured at configuration time
}
}
}
// GOOD: Lazy evaluation with providers
class GoodPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create("good", GoodExtension::class.java)
project.tasks.register("good") {
doLast {
// Evaluated at execution time
println(extension.message.get())
}
}
}
}
// BAD: No default values
abstract class ExtensionWithoutDefaults {
abstract val required: Property<String>
// User MUST set this or build fails - poor UX
}
// GOOD: Provide sensible defaults
abstract class ExtensionWithDefaults {
abstract val optional: Property<String>
init {
optional.convention("sensible-default") // User can override if needed
}
}
```
#### Extension with Nested Configuration
```kotlin
// Nested extension for database configuration
abstract class DatabaseExtension {
abstract val host: Property<String>
abstract val port: Property<Int>
abstract val username: Property<String>
abstract val password: Property<String>
}
// Main extension
abstract class AppExtension(objects: ObjectFactory) {
// Simple properties
abstract val appName: Property<String>
abstract val version: Property<String>
// Nested object (always created)
val database: DatabaseExtension = objects.newInstance(DatabaseExtension::class.java)
// Configure nested object with DSL
fun database(action: Action<DatabaseExtension>) {
action.execute(database)
}
init {
appName.convention("MyApp")
version.convention("1.0.0")
database.port.convention(5432)
}
}
// Usage in build.gradle.kts
app {
appName = "CoolApp"
version = "2.0.0"
database {
host = "localhost"
port = 5432
username = "admin"
password = providers.gradleProperty("db.password").orElse("default")
}
}
```
#### Extension with Named Domain Objects
For collections of similar configurations:
```kotlin
import org.gradle.api.NamedDomainObjectContainer
// Define a server configuration
abstract class ServerConfig(val name: String) {
abstract val host: Property<String>
abstract val port: Property<Int>
init {
port.convention(8080)
}
}
// Extension with container
abstract class DeploymentExtension(objects: ObjectFactory) {
// Container of servers
val servers: NamedDomainObjectContainer<ServerConfig> =
objects.domainObjectContainer(ServerConfig::class.java)
// DSL method for configuring servers
fun servers(action: Action<NamedDomainObjectContainer<ServerConfig>>) {
action.execute(servers)
}
}
// Usage in build.gradle.kts
deployment {
servers {
create("production") {
host = "prod.example.com"
port = 443
}
create("staging") {
host = "staging.example.com"
port = 8080
}
}
}
// Access servers in task
tasks.register("deployToProduction") {
doLast {
val prodServer = extensions.getByType<DeploymentExtension>()
.servers.getByName("production")
println("Deploying to ${prodServer.host.get()}:${prodServer.port.get()}")
}
}
```
#### Extension Best Practices
```kotlin
abstract class WellDesignedExtension @Inject constructor(
private val objects: ObjectFactory,
private val providers: ProviderFactory
) {
// GOOD: Use Property<T> for mutable configuration
abstract val apiKey: Property<String>
// GOOD: Provide sensible defaults
abstract val timeout: Property<Int>
// GOOD: Use Provider for derived values
val apiUrl: Provider<String> = apiKey.map { key ->
"https://api.example.com?key=$key"
}
// GOOD: Validate in finalizer (not during configuration)
init {
timeout.convention(30)
// Validation happens when value is accessed
apiKey.finalizeValueOnRead()
}
// GOOD: Provide configuration methods with clear names
fun useDefaultCredentials() {
apiKey.set(providers.environmentVariable("API_KEY"))
}
fun useCustomCredentials(key: String) {
apiKey.set(key)
}
}
// BAD: Using plain variables (not lazy)
// class BadExtension {
// var apiKey: String = "" // Not lazy, no defaults, no validation
// }
```
#### Connecting Extension to Tasks
```kotlin
abstract class PublishExtension {
abstract val version: Property<String>
abstract val repository: Property<String>
init {
version.convention("1.0.0")
repository.convention("https://repo.example.com")
}
}
class PublishPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create("publish", PublishExtension::class.java)
project.tasks.register<PublishTask>("publish") {
// GOOD: Wire extension properties to task properties
version.set(extension.version)
repository.set(extension.repository)
}
}
}
abstract class PublishTask : DefaultTask() {
@get:Input
abstract val version: Property<String>
@get:Input
abstract val repository: Property<String>
@TaskAction
fun publish() {
println("Publishing version ${version.get()} to ${repository.get()}")
}
}
```
### Providers API
The Providers API enables lazy configuration, which is essential for configuration cache and fast builds.
#### Provider<T> Basics
```kotlin
// GOOD: Provider wraps a value that's computed lazily
val messageProvider: Provider<String> = providers.provider {
"Message computed at ${System.currentTimeMillis()}"
}
// Value is only computed when accessed
tasks.register("printMessage") {
doLast {
println(messageProvider.get()) // Computed here
}
}
// GOOD: Provider from environment variable
val apiKeyProvider: Provider<String> = providers.environmentVariable("API_KEY")
// GOOD: Provider from system property
val debugProvider: Provider<String> = providers.systemProperty("debug")
// GOOD: Provider from gradle property
val versionProvider: Provider<String> = providers.gradleProperty("app.version")
```
#### Property<T> for Mutable Values
```kotlin
abstract class ConfigurableTask : DefaultTask() {
// GOOD: Property<T> for task inputs (can be set and connected)
@get:Input
abstract val message: Property<String>
@get:Input
abstract val count: Property<Int>
init {
// Set default values
message.convention("Default message")
count.convention(1)
}
@TaskAction
fun execute() {
repeat(count.get()) {
println(message.get())
}
}
}
// Configure task
tasks.register<ConfigurableTask>("configurable") {
message.set("Hello from property")
count.set(5)
}
```
#### Transforming Providers
```kotlin
// GOOD: map - transform provider value
val version: Provider<String> = providers.gradleProperty("version")
val fullVersion: Provider<String> = version.map { v ->
"v$v-${System.currentTimeMillis()}"
}
// GOOD: flatMap - chain providers
val baseUrl: Provider<String> = providers.gradleProperty("baseUrl")
val apiUrl: Provider<String> = baseUrl.flatMap { base ->
providers.provider { "$base/api/v1" }
}
// GOOD: orElse - provide fallback
val timeout: Provider<Int> = providers.gradleProperty("timeout")
.map { it.toInt() }
.orElse(30)
// GOOD: zip - combine two providers
val host: Provider<String> = providers.gradleProperty("host")
val port: Provider<Int> = providers.gradleProperty("port").map { it.toInt() }
val endpoint: Provider<String> = host.zip(port) { h, p ->
"$h:$p"
}
```
#### Connecting Providers
```kotlin
abstract class SourceTask : DefaultTask() {
@get:Input
abstract val sourceMessage: Property<String>
init {
sourceMessage.convention("Source data")
}
@TaskAction
fun execute() {
println("Source: ${sourceMessage.get()}")
}
}
abstract class TargetTask : DefaultTask() {
@get:Input
abstract val targetMessage: Property<String>
@TaskAction
fun execute() {
println("Target: ${targetMessage.get()}")
}
}
// GOOD: Connect provider from one task to another
val sourceTask = tasks.register<SourceTask>("source")
tasks.register<TargetTask>("target") {
// Wire output from source to input of target
targetMessage.set(sourceTask.flatMap { it.sourceMessage })
}
```
#### File and Directory Providers
```kotlin
abstract class FileTask : DefaultTask() {
// GOOD: Use RegularFileProperty for files
@get:OutputFile
abstract val outputFile: RegularFileProperty
// GOOD: Use DirectoryProperty for directories
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun execute() {
// Get file and directory
val file = outputFile.get().asFile
val dir = outputDir.get().asFile
file.writeText("Output content")
println("Wrote to ${file.absolutePath}")
}
}
tasks.register<FileTask>("fileTask") {
// GOOD: Use layout.buildDirectory for build outputs
outputFile.set(layout.buildDirectory.file("output.txt"))
outputDir.set(layout.buildDirectory.dir("outputs"))
}
// GOOD: Map file providers
tasks.register("processFile") {
val inputProvider: Provider<RegularFile> = layout.buildDirectory.file("input.txt")
val outputProvider: Provider<RegularFile> = inputProvider.map { input ->
layout.buildDirectory.file("processed-${input.asFile.name}").get()
}
}
```
#### Collection Providers
```kotlin
abstract class CollectionTask : DefaultTask() {
// GOOD: ListProperty for list of values
@get:Input
abstract val items: ListProperty<String>
// GOOD: SetProperty for unique values
@get:Input
abstract val tags: SetProperty<String>
// GOOD: MapProperty for key-value pairs
@get:Input
abstract val config: MapProperty<String, String>
@TaskAction
fun execute() {
println("Items: ${items.get()}")
println("Tags: ${tags.get()}")
println("Config: ${config.get()}")
}
}
tasks.register<CollectionTask>("collections") {
// Set collections
items.set(listOf("a", "b", "c"))
items.add("d") // Add single item
tags.set(setOf("gradle", "kotlin"))
tags.add("build")
config.set(mapOf("env" to "prod", "region" to "us"))
config.put("version", "1.0")
}
```
#### Common Anti-Patterns to Avoid
```kotlin
// BAD: Eager evaluation during configuration
// val version = project.findProperty("version") as String // Evaluated immediately
// GOOD: Lazy evaluation with provider
val version: Provider<String> = providers.gradleProperty("version")
// BAD: Calling .get() during configuration phase
// tasks.register("bad") {
// val msg = messageProvider.get() // Forces evaluation too early
// doLast { println(msg) }
// }
// GOOD: Call .get() only in task action
tasks.register("good") {
doLast {
println(messageProvider.get()) // Evaluated at execution time
}
}
// BAD: Using plain variables in task configuration
// var myVar = "value"
// tasks.register("bad") {
// doLast { println(myVar) } // Captures current value, not lazy
// }
// GOOD: Using properties
abstract class GoodTask : DefaultTask() {
@get:Input
abstract val myProperty: Property<String>
@TaskAction
fun execute() {
println(myProperty.get()) // Lazy, cached, compatible with config cache
}
}
```
#### Provider Best Practices
```kotlin
// GOOD: Use providers for external inputs
val externalConfig: Provider<String> = providers.fileContents(
layout.projectDirectory.file("config.txt")
).asText
// GOOD: Finalize values to catch configuration errors early
val criticalValue: Property<String> = objects.property(String::class.java)
criticalValue.finalizeValueOnRead() // Value can't change after first read
// GOOD: Use conventions for defaults
val timeout: Property<Int> = objects.property(Int::class.java)
timeout.convention(30) // Default value if not set
// GOOD: Validate provider values
val port: Provider<Int> = providers.gradleProperty("port")
.map { it.toInt() }
.map { p ->
require(p in 1..65535) { "Port must be between 1 and 65535" }
p
}
// GOOD: Use providers.provider for expensive computations
val expensiveValue: Provider<String> = providers.provider {
// This expensive computation only runs when needed
Thread.sleep(100)
"Computed value"
}
```
### Gradle Caching
Caching is essential for fast Gradle builds. There are two types of caching: **build cache** (task outputs) and **configuration cache** (build configuration).
#### Build Cache Basics
The build cache stores task outputs and reuses them when inputs haven't changed.
**Enable build cache (gradle.properties):**
```properties
org.gradle.caching=true
```
**Or via command line:**
```bash
./gradlew build --build-cache
```
**How it works:**
1. Gradle calculates cache key from task inputs
2. If cache hit: Reuses outputs, task shows "FROM-CACHE"
3. If cache miss: Executes task, stores outputs
#### Writing Cache-Compatible Tasks
```kotlin
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
// GOOD: Cache-compatible task with proper annotations
@CacheableTask // Mark class as cacheable (must import org.gradle.api.tasks.CacheableTask)
abstract class CacheableProcessTask : DefaultTask() {
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val inputFiles: ConfigurableFileCollection
@get:Input
abstract val processMode: Property<String>
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun process() {
val output = outputDir.get().asFile
output.mkdirs()
inputFiles.forEach { file ->
val processed = output.resolve(file.name)
when (processMode.get()) {
"uppercase" -> processed.writeText(file.readText().uppercase())
"lowercase" -> processed.writeText(file.readText().lowercase())
}
}
}
}
// BAD: Task that's not cacheable (no @CacheableTask, uses external state)
abstract class BadTask : DefaultTask() {
@TaskAction
fun execute() {
val timestamp = System.currentTimeMillis() // Non-deterministic!
File("output.txt").writeText("Built at $timestamp")
}
}
```
#### What Makes Tasks Cacheable
✅ **GOOD - Cacheable:**
- Deterministic outputs (same inputs → same outputs)
- All inputs properly annotated (@InputFiles, @Input, etc.)
- No external state (environment, timestamps, random)
- Uses Provider API for configuration
- Marked with @CacheableTask at class level
- Uses @PathSensitive for file inputs
❌ **BAD - Not Cacheable:**
- Non-deterministic (timestamps, random values, System.currentTimeMillis())
- Missing input/output annotations
- Depends on external state not declared as inputs
- Modifies state outside task outputs
- No @CacheableTask annotation at class level
#### Making Built-in Tasks Cacheable
```kotlin
// GOOD: Configure tasks to be cacheable
tasks.withType<Test>().configureEach {
outputs.cacheIf { true } // Enable caching for tests
}
// GOOD: Normalize file paths for cache portability
normalization {
runtimeClasspath {
ignore("META-INF/MANIFEST.MF") // Ignore non-functional differences
}
}
```
#### Configuration Cache
Configuration cache stores the configured task graph, eliminating configuration phase on subsequent builds.
**Enable configuration cache (gradle.properties):**
```properties
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn # Or 'fail'
```
**Or via command line:**
```bash
./gradlew build --configuration-cache
```
**Benefits:**
- Up to 90% faster builds (no configuration phase)
- Second build reuses cached configuration
- Encourages better build practices
#### Configuration Cache Compatibility
```kotlin
// GOOD: Configuration cache compatible
tasks.register("compatible") {
val message: Provider<String> = providers.gradleProperty("message")
doLast {
println(message.get()) // Lazy evaluation
}
}
// BAD: Not configuration cache compatible
// tasks.register("incompatible") {
// val message = project.findProperty("message") // Eager evaluation
// doLast {
// println(message) // Captures project state at configuration time
// }
// }
// GOOD: Use build services for shared state
interface MyBuildService : BuildService<BuildServiceParameters.None> {
fun performWork() {
println("Build service performing work")
}
}
abstract class SharedStateTask : DefaultTask() {
@get:Internal // Build services are not inputs
abstract val myService: Property<MyBuildService>
@TaskAction
fun execute() {
myService.get().performWork()
}
}
// Register the build service
val myServiceProvider = gradle.sharedServices.registerIfAbsent("myService", MyBuildService::class) {
// Configure service parameters here if needed
}
// Wire service to task
tasks.register<SharedStateTask>("taskWithService") {
myService.set(myServiceProvider)
}
// BAD: Using static/global state instead of build services
object BadSharedState {
var counter = 0 // Mutable global state - not serializable!
}
// Problem: Breaks configuration cache, not thread-safe, not isolated
// BAD: Trying to share data via files without proper task dependencies
tasks.register("badProducer") {
doLast {
File("shared.txt").writeText("data") // No output annotation!
}
}
tasks.register("badConsumer") {
doLast {
val data = File("shared.txt").readText() // No input annotation!
println(data)
}
}
// Problem: No dependency declared, may run in wrong order or break caching
// GOOD: Use task outputs/inputs or build services for shared state
```
#### Common Configuration Cache Issues
```kotlin
// PROBLEM: Accessing project at execution time
// tasks.register("bad") {
// doLast {
// println(project.name) // Configuration cache error!
// }
// }
// SOLUTION: Capture value during configuration
tasks.register("good") {
val projectName = project.name // Captured during configuration
doLast {
println(projectName) // OK - uses captured value
}
}
// PROBLEM: Using mutable shared state
// val sharedList = mutableListOf<String>()
// tasks.register("bad") {
// doLast {
// sharedList.add("item") // Not serializable!
// }
// }
// SOLUTION: Use build services or task outputs
abstract class GoodTask : DefaultTask() {
@get:OutputFile
abstract val outputFile: RegularFileProperty
@TaskAction
fun execute() {
outputFile.get().asFile.appendText("item\n")
}
}
```
#### Cache Debugging
```bash
# Check what's not cacheable
./gradlew build --build-cache --info | grep "Caching disabled"
# Explain why task wasn't cached
./gradlew help --task processFiles
# Clear build cache
rm -rf ~/.gradle/caches/build-cache-*
rm -rf .gradle/build-cache
# Check configuration cache problems
./gradlew build --configuration-cache --configuration-cache-problems=warn
# Rerun without cache to compare
./gradlew clean build --no-build-cache --no-configuration-cache
./gradlew clean build --build-cache --configuration-cache
```
#### Remote Build Cache
```kotlin
// settings.gradle.kts
buildCache {
local {
isEnabled = true
}
remote<HttpBuildCache> {
url = uri("https://cache.example.com/")
isPush = providers.environmentVariable("CI")
.map { it == "true" }
.getOrElse(false)
credentials {
username = providers.environmentVariable("CACHE_USER").orNull
password = providers.environmentVariable("CACHE_PASSWORD").orNull
}
}
}
// Benefits:
// - Share cache across CI and developers
// - Dramatically faster CI builds
// - Consistent build performance
```
#### Cache Performance Tips
```kotlin
// GOOD: Use relative path sensitivity when possible
abstract class OptimizedTask : DefaultTask() {
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE) // Better cache hits
abstract val sources: ConfigurableFileCollection
}
// GOOD: Exclude non-functional files
normalization {
runtimeClasspath {
ignore("**/*.txt") // If .txt files don't affect behavior
ignore("META-INF/MANIFEST.MF")
}
}
// GOOD: Use file collections instead of file trees for better caching
val sources: ConfigurableFileCollection = objects.fileCollection()
sources.from(fileTree("src") { include("**/*.java") })
// GOOD: Split large tasks into smaller cacheable units
tasks.register("compileAll") {
dependsOn("compileModule1", "compileModule2", "compileModule3")
}
// Each module compiled separately = better cache granularity
```
#### Measuring Cache Effectiveness
```bash
# Build scan (best way to analyze caching)
./gradlew build --scan
# The build scan shows:
# - Cache hit rate
# - Which tasks were cached
# - Why tasks were not cached
# - Performance timeline
# Enable with:
# plugins {
# id("com.gradle.develocity") version "3.16"
# }
#
# develocity {
# buildScan {
# publishing.onlyIf { true }
# }
# }
```
### Custom Plugins
Custom plugins encapsulate build logic for reuse across projects or modules.
#### Binary Plugin (Plugin<Project>)
```kotlin
// buildSrc/src/main/kotlin/GreetingPlugin.kt
import org.gradle.api.Plugin
import org.gradle.api.Project
class GreetingPlugin : Plugin<Project> {
override fun apply(project: Project) {
// Create extension for configuration
val extension = project.extensions.create(
"greeting",
GreetingExtension::class.java
)
// Register task that uses extension
project.tasks.register("greet") {
group = "custom"
description = "Prints a greeting message"
doLast {
repeat(extension.times.get()) {
println(extension.message.get())
}
}
}
}
}
// Extension for configuration
abstract class GreetingExtension {
abstract val message: Property<String>
abstract val times: Property<Int>
init {
message.convention("Hello from plugin")
times.convention(1)
}
}
// buildSrc/src/main/resources/META-INF/gradle-plugins/greeting.properties
implementation-class=GreetingPlugin
// Usage in build.gradle.kts:
// plugins {
// id("greeting")
// }
//
// greeting {
// message = "Hello, World!"
// times = 3
// }
```
#### Precompiled Script Plugin (Recommended for Simple Plugins)
Easier approach using Kotlin DSL directly:
```kotlin
// buildSrc/src/main/kotlin/java-library-conventions.gradle.kts
plugins {
`java-library`
`maven-publish`
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
withSourcesJar()
withJavadocJar()
}
repositories {
mavenCentral()
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.test {
useJUnitPlatform()
}
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
}
}
}
// Usage in build.gradle.kts:
// plugins {
// id("java-library-conventions")
// }
```
#### Complete Plugin Example
```kotlin
// buildSrc/src/main/kotlin/DocumentationPlugin.kt
import org.gradle.api.*
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
class DocumentationPlugin : Plugin<Project> {
override fun apply(project: Project) {
// Register extension
val extension = project.extensions.create(
"documentation",
DocumentationExtension::class.java
)
// Configure defaults from extension
extension.sourceDir.convention(
project.layout.projectDirectory.dir("docs")
)
extension.outputDir.convention(
project.layout.buildDirectory.dir("docs")
)
// Register task
project.tasks.register<GenerateDocsTask>("generateDocs") {
sourceDir.set(extension.sourceDir)
outputDir.set(extension.outputDir)
format.set(extension.format)
group = "documentation"
description = "Generates documentation"
}
// Hook into build lifecycle
project.tasks.named("build") {
dependsOn("generateDocs")
}
}
}
abstract class DocumentationExtension {
abstract val sourceDir: DirectoryProperty
abstract val outputDir: DirectoryProperty
abstract val format: Property<String>
init {
format.convention("html")
}
}
abstract class GenerateDocsTask : DefaultTask() {
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val sourceDir: DirectoryProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@get:Input
abstract val format: Property<String>
@TaskAction
fun generate() {
val output = outputDir.get().asFile
output.mkdirs()
sourceDir.get().asFileTree.forEach { file ->
val outputFile = output.resolve("${file.nameWithoutExtension}.${format.get()}")
outputFile.writeText("Generated from ${file.name}")
}
logger.lifecycle("Generated documentation in ${output.absolutePath}")
}
}
```
#### Plugin with Build Service
For shared state across tasks:
```kotlin
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters
abstract class MetricsService : BuildService<BuildServiceParameters.None> {
private val metrics = mutableMapOf<String, Long>()
fun record(metric: String, value: Long) {
metrics[metric] = value
}
fun report() {
println("Build Metrics:")
metrics.forEach { (key, value) ->
println(" $key: $value")
}
}
}
class MetricsPlugin : Plugin<Project> {
override fun apply(project: Project) {
// Register build service
val metricsService = project.gradle.sharedServices.registerIfAbsent(
"metrics",
MetricsService::class.java
) {}
// Use service in tasks
project.tasks.register<MetricsTask>("recordMetrics") {
this.metricsService.set(metricsService)
}
// Report at end of build
project.gradle.buildFinished {
metricsService.get().report()
}
}
}
abstract class MetricsTask : DefaultTask() {
@get:ServiceReference("metrics")
abstract val metricsService: Property<MetricsService>
@TaskAction
fun record() {
metricsService.get().record("task_count", 42)
}
}
```
#### Testing Custom Plugins
```kotlin
// buildSrc/src/test/kotlin/GreetingPluginTest.kt
import org.gradle.testfixtures.ProjectBuilder
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
class GreetingPluginTest {
@Test
fun `plugin registers greet task`() {
val project = ProjectBuilder.builder().build()
project.pluginManager.apply("greeting")
val task = project.tasks.findByName("greet")
assertNotNull(task)
}
@Test
fun `extension has default values`() {
val project = ProjectBuilder.builder().build()
project.pluginManager.apply("greeting")
val extension = project.extensions.getByType(GreetingExtension::class.java)
assertEquals("Hello from plugin", extension.message.get())
assertEquals(1, extension.times.get())
}
@Test
fun `can configure extension`() {
val project = ProjectBuilder.builder().build()
project.pluginManager.apply("greeting")
val extension = project.extensions.getByType(GreetingExtension::class.java)
extension.message.set("Custom message")
extension.times.set(5)
assertEquals("Custom message", extension.message.get())
assertEquals(5, extension.times.get())
}
}
```
#### Publishing Plugins
```kotlin
// buildSrc/build.gradle.kts (for publishing to plugin portal)
plugins {
`kotlin-dsl`
`maven-publish`
id("com.gradle.plugin-publish") version "1.2.1"
}
group = "com.example"
version = "1.0.0"
gradlePlugin {
website = "https://github.com/example/plugin"
vcsUrl = "https://github.com/example/plugin"
plugins {
create("greetingPlugin") {
id = "com.example.greeting"
displayName = "Greeting Plugin"
description = "A plugin that greets users"
tags = listOf("greeting", "example")
implementationClass = "com.example.GreetingPlugin"
}
}
}
publishing {
repositories {
maven {
name = "Internal"
url = uri("https://repo.company.com/maven")
}
}
}
// Publish with: ./gradlew publishPlugins
```
#### Plugin Best Practices
```kotlin
class WellDesignedPlugin : Plugin<Project> {
override fun apply(project: Project) {
// GOOD: Validate environment
require(project.hasProperty("requiredProp")) {
"Plugin requires 'requiredProp' property"
}
// GOOD: Use lazy registration
val extension = project.extensions.create("wellDesigned", Extension::class.java)
// GOOD: Register tasks lazily
project.tasks.register("myTask") {
// Configure with extension
}
// GOOD: Apply other plugins if needed
project.pluginManager.apply("java")
// GOOD: Configure other plugins
project.plugins.withType<JavaPlugin> {
project.java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
}
// GOOD: Hook into lifecycle cleanly
project.afterEvaluate {
// Configuration that needs evaluated project
}
}
}
// BAD: Applying plugins eagerly
// project.apply(plugin = "java") // Use pluginManager.apply()
// BAD: Configuring in constructor
// class BadPlugin : Plugin<Project> {
// init {
// // Plugin not yet applied!
// }
// }
```
### Build Logic Reuse
There are multiple strategies for sharing build logic across projects and modules.
#### Strategy 1: buildSrc (Simplest)
Best for: Single repository, convention plugins, shared code within one project.
```
project/
├── buildSrc/
│ ├── build.gradle.kts
│ ├── settings.gradle.kts
│ └── src/
│ └── main/kotlin/
│ ├── java-conventions.gradle.kts
│ ├── kotlin-conventions.gradle.kts
│ └── MyCustomPlugin.kt
├── app/
│ └── build.gradle.kts
└── lib/
└── build.gradle.kts
```
**buildSrc/build.gradle.kts:**
```kotlin
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
// Add dependencies needed by your plugins
implementation("com.github.johnrengelman:shadow:8.1.1")
}
```
**buildSrc/settings.gradle.kts:**
```kotlin
rootProject.name = "buildSrc"
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
mavenCentral()
gradlePluginPortal()
}
}
```
**buildSrc/src/main/kotlin/java-conventions.gradle.kts:**
```kotlin
plugins {
java
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
}
tasks.test {
useJUnitPlatform()
}
```
**Usage in app/build.gradle.kts:**
```kotlin
plugins {
id("java-conventions") // Automatically available
application
}
```
**Pros:**
- Simple setup
- Automatically available to all modules
- Fast incremental builds
**Cons:**
- Tied to single project
- Changes require Gradle daemon restart
- Can't be versioned separately
#### Strategy 2: Included Builds (Flexible)
Best for: Multi-repo setups, versioned build logic, independent releases.
```
company-builds/
├── my-project/
│ ├── settings.gradle.kts (includes build-logic)
│ ├── app/
│ └── lib/
└── build-logic/
├── settings.gradle.kts
├── build.gradle.kts
└── src/
└── main/kotlin/
└── conventions/
├── java-conventions.gradle.kts
└── kotlin-conventions.gradle.kts
```
**my-project/settings.gradle.kts:**
```kotlin
rootProject.name = "my-project"
// Include build-logic
includeBuild("../build-logic")
include("app")
include("lib")
```
**build-logic/settings.gradle.kts:**
```kotlin
rootProject.name = "build-logic"
dependencyResolutionManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
```
**build-logic/build.gradle.kts:**
```kotlin
plugins {
`kotlin-dsl`
}
group = "com.example.build"
version = "1.0.0"
dependencies {
implementation("com.github.johnrengelman:shadow:8.1.1")
}
gradlePlugin {
plugins {
register("javaConventions") {
id = "com.example.java-conventions"
implementationClass = "conventions.JavaConventionsPlugin"
}
}
}
```
**Usage in my-project/app/build.gradle.kts:**
```kotlin
plugins {
id("com.example.java-conventions")
}
```
**Pros:**
- Independent versioning
- No daemon restart needed
- Shareable across projects
- Can be published
**Cons:**
- More complex setup
- Need to manage versions
#### Strategy 3: Published Plugins (Enterprise)
Best for: Many projects, organization-wide standards, versioned releases.
**Plugin Project Structure:**
```
gradle-plugins/
├── settings.gradle.kts
├── build.gradle.kts
└── src/
└── main/
├── kotlin/
│ └── com/example/plugins/
│ └── JavaConventionsPlugin.kt
└── resources/
└── META-INF/gradle-plugins/
└── com.example.java-conventions.properties
```
**build.gradle.kts:**
```kotlin
plugins {
`kotlin-dsl`
`maven-publish`
}
group = "com.example.gradle"
version = "1.0.0"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(11) // Wide compatibility
}
}
publishing {
repositories {
maven {
name = "Company"
url = uri("https://repo.company.com/maven")
credentials {
username = System.getenv("REPO_USER")
password = System.getenv("REPO_PASSWORD")
}
}
}
publications {
create<MavenPublication>("plugin") {
from(components["java"])
}
}
}
```
**Consumer settings.gradle.kts:**
```kotlin
pluginManagement {
repositories {
maven {
url = uri("https://repo.company.com/maven")
}
gradlePluginPortal()
}
}
```
**Consumer build.gradle.kts:**
```kotlin
plugins {
id("com.example.java-conventions") version "1.0.0"
}
```
**Pros:**
- Enterprise-grade
- Versioned releases
- Change management
- Works across all projects
**Cons:**
- Most complex
- Release overhead
- Version management needed
#### Strategy 4: Composite Builds (Advanced)
Best for: Multiple independent projects that need to work together.
```
workspace/
├── project-a/
│ ├── settings.gradle.kts
│ └── build.gradle.kts
├── project-b/
│ ├── settings.gradle.kts
│ └── build.gradle.kts
└── shared-library/
├── settings.gradle.kts
└── build.gradle.kts
```
**project-a/settings.gradle.kts:**
```kotlin
rootProject.name = "project-a"
// Include other project as composite build
includeBuild("../shared-library")
```
**project-a/build.gradle.kts:**
```kotlin
dependencies {
// Depend on shared library
implementation("com.example:shared-library:1.0.0")
// Gradle substitutes with composite build automatically
}
```
**Pros:**
- Independent projects
- Source dependencies
- IDE integration
- Parallel development
**Cons:**
- Complex setup
- Dependency substitution rules needed
#### Choosing the Right Strategy
| Scenario | Recommended Strategy |
|----------|---------------------|
| Single repo, simple conventions | buildSrc |
| Multi-repo, same org | Included builds |
| Organization-wide standards | Published plugins |
| Multiple independent projects | Composite builds |
| Experimenting with new patterns | buildSrc → Included builds |
#### Convention Plugin Patterns
**Important:** Precompiled script plugins in buildSrc automatically get plugin IDs based on their file path. A file at `buildSrc/src/main/kotlin/conventions/java-base.gradle.kts` becomes plugin `id("conventions.java-base")`.
```kotlin
// Pattern 1: Pure configuration plugin
// Location: buildSrc/src/main/kotlin/conventions/java-base.gradle.kts
// Plugin ID: conventions.java-base (auto-generated from file path)
plugins {
java
}
java {
toolchain.languageVersion = JavaLanguageVersion.of(21)
}
// Pattern 2: Conditional configuration
// Location: buildSrc/src/main/kotlin/conventions/java-app.gradle.kts
// Plugin ID: conventions.java-app
plugins {
id("conventions.java-base") // References Pattern 1 by its auto-generated ID
application
}
if (project.hasProperty("enableJacoco")) {
apply(plugin = "jacoco")
}
// Pattern 3: Composed plugins
// conventions/java-library.gradle.kts
plugins {
id("conventions.java-base")
`java-library`
`maven-publish`
}
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
}
}
}
```
#### Sharing Configuration Files
```kotlin
// buildSrc/src/main/resources/checkstyle.xml
// buildSrc/src/main/kotlin/conventions.gradle.kts
plugins {
checkstyle
}
checkstyle {
configFile = file("${project.rootDir}/buildSrc/src/main/resources/checkstyle.xml")
toolVersion = "10.12.5"
}
// All modules get consistent checkstyle configuration
```
#### Version Management for Shared Logic
```kotlin
// build-logic/build.gradle.kts
version = "1.2.0" // Increment when making changes
// Consumers can pin versions
// settings.gradle.kts
pluginManagement {
resolutionStrategy {
eachPlugin {
if (requested.id.id == "com.example.conventions") {
useVersion("1.2.0")
}
}
}
}
```
---
## Groovy → Kotlin DSL Migration Guide
This section helps developers migrate existing Groovy DSL build scripts to Kotlin DSL.
### Syntax Differences
This section provides side-by-side comparisons of common syntax patterns.
#### Basic Syntax Table
| Feature | Groovy DSL | Kotlin DSL |
|---------|------------|------------|
| **File name** | `build.gradle` | `build.gradle.kts` |
| **Settings file** | `settings.gradle` | `settings.gradle.kts` |
| **Assignment** | `version = '1.0'` | `version = "1.0"` |
| **String literals** | `'single'` or `"double"` | `"double"` only |
| **String interpolation** | `"Version $version"` | `"Version $version"` |
| **Method calls** | `implementation 'lib'` | `implementation("lib")` |
| **Configuration blocks** | `java { ... }` | `java { ... }` |
| **Task configuration** | `task myTask { ... }` | `tasks.register("myTask") { ... }` |
#### Assignment and Properties
**Groovy:**
```groovy
version = '1.0.0'
group = 'com.example'
ext.customProp = 'value'
ext {
anotherProp = 'value'
}
```
**Kotlin:**
```kotlin
version = "1.0.0"
group = "com.example"
extra["customProp"] = "value"
// Or with type-safe accessor
val customProp by extra("value")
```
#### String Literals
**Groovy:**
```groovy
// Both work in Groovy
implementation 'com.google.guava:guava:33.0.0-jre'
implementation "com.google.guava:guava:33.0.0-jre"
// String interpolation
def myVersion = '1.0'
println "Version: $myVersion"
```
**Kotlin:**
```kotlin
// Only double quotes work in Kotlin
implementation("com.google.guava:guava:33.0.0-jre")
// String interpolation (same as Groovy)
val myVersion = "1.0"
println("Version: $myVersion")
```
#### Method Call Syntax
**Groovy (implicit parentheses):**
```groovy
// Groovy allows omitting parentheses
implementation 'com.google.guava:guava:33.0.0-jre'
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
// Configuration blocks
repositories {
mavenCentral()
}
```
**Kotlin (explicit parentheses):**
```kotlin
// Kotlin requires parentheses for method calls
implementation("com.google.guava:guava:33.0.0-jre")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
// Configuration blocks (same)
repositories {
mavenCentral()
}
```
#### Property Access vs Method Calls
**Groovy:**
```groovy
// Groovy uses property syntax for getters/setters
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
tasks.test {
maxParallelForks = 4
}
```
**Kotlin:**
```kotlin
// Kotlin also uses property syntax
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
tasks.named<Test>("test") {
maxParallelForks = 4
}
```
#### Collection Literals
**Groovy:**
```groovy
// List
def myList = ['item1', 'item2', 'item3']
// Map
def myMap = [key1: 'value1', key2: 'value2']
```
**Kotlin:**
```kotlin
// List
val myList = listOf("item1", "item2", "item3")
// Map
val myMap = mapOf("key1" to "value1", "key2" to "value2")
```
#### Configuration Delegation
**Groovy (implicit delegate):**
```groovy
tasks.create('myTask') {
// 'doLast' resolves through task delegate
doLast {
println 'Task executed'
}
}
```
**Kotlin (explicit receiver):**
```kotlin
tasks.register("myTask") {
// 'this' refers to the task
doLast {
println("Task executed")
}
}
```
### Plugin Application Conversion
#### Old apply Syntax to plugins Block
**Groovy (old style):**
```groovy
apply plugin: 'java'
apply plugin: 'application'
apply plugin: 'com.github.johnrengelman.shadow'
buildscript {
repositories {
gradlePluginPortal()
}
dependencies {
classpath 'com.github.johnrengelman:shadow:8.1.1'
}
}
```
**Kotlin (modern style):**
```kotlin
plugins {
java
application
id("com.github.johnrengelman.shadow") version "8.1.1"
}
// No buildscript block needed for plugins from Gradle Plugin Portal
```
#### Core Plugins
**Groovy:**
```groovy
apply plugin: 'java'
apply plugin: 'java-library'
apply plugin: 'application'
apply plugin: 'groovy'
```
**Kotlin:**
```kotlin
plugins {
java
`java-library` // Note backticks for kebab-case
application
groovy
}
```
#### Kotlin Plugins
**Groovy:**
```groovy
apply plugin: 'org.jetbrains.kotlin.jvm'
buildscript {
dependencies {
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.0'
}
}
```
**Kotlin:**
```kotlin
plugins {
kotlin("jvm") version "2.0.0"
// Short form for Kotlin plugins
// Alternative: id("org.jetbrains.kotlin.jvm") version "2.0.0"
}
```
#### Applying Plugins Conditionally
**Groovy:**
```groovy
if (project.hasProperty('enableKotlin')) {
apply plugin: 'org.jetbrains.kotlin.jvm'
}
```
**Kotlin:**
```kotlin
plugins {
java
if (project.hasProperty("enableKotlin")) {
kotlin("jvm") version "2.0.0"
}
}
// Alternative: Apply outside plugins block
if (project.findProperty("enableKotlin") == "true") {
apply(plugin = "org.jetbrains.kotlin.jvm")
}
```
#### apply false for Root Projects
**Groovy:**
```groovy
plugins {
id 'org.springframework.boot' version '3.2.0' apply false
}
```
**Kotlin:**
```kotlin
plugins {
id("org.springframework.boot") version "3.2.0" apply false
}
```
### Dependency Declaration Differences
#### Basic Dependency Notation
**Groovy:**
```groovy
dependencies {
implementation 'com.google.guava:guava:33.0.0-jre'
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
compileOnly 'org.projectlombok:lombok:1.18.30'
runtimeOnly 'com.h2database:h2:2.2.224'
}
```
**Kotlin:**
```kotlin
dependencies {
implementation("com.google.guava:guava:33.0.0-jre")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
compileOnly("org.projectlombok:lombok:1.18.30")
runtimeOnly("com.h2database:h2:2.2.224")
}
```
#### Map Notation
**Groovy:**
```groovy
dependencies {
implementation group: 'com.google.guava', name: 'guava', version: '33.0.0-jre'
implementation([group: 'com.google.guava', name: 'guava', version: '33.0.0-jre'])
}
```
**Kotlin:**
```kotlin
dependencies {
implementation(group = "com.google.guava", name = "guava", version = "33.0.0-jre")
// Or stick with string notation (more common)
implementation("com.google.guava:guava:33.0.0-jre")
}
```
#### Excluding Dependencies
**Groovy:**
```groovy
dependencies {
implementation('com.example:library:1.0') {
exclude group: 'commons-logging', module: 'commons-logging'
}
}
```
**Kotlin:**
```kotlin
dependencies {
implementation("com.example:library:1.0") {
exclude(group = "commons-logging", module = "commons-logging")
}
}
```
#### Platform/BOM Dependencies
**Groovy:**
```groovy
dependencies {
implementation platform('org.springframework.boot:spring-boot-dependencies:3.2.0')
implementation 'org.springframework.boot:spring-boot-starter-web'
}
```
**Kotlin:**
```kotlin
dependencies {
implementation(platform("org.springframework.boot:spring-boot-dependencies:3.2.0"))
implementation("org.springframework.boot:spring-boot-starter-web")
}
```
#### Project Dependencies
**Groovy:**
```groovy
dependencies {
implementation project(':common')
implementation project(path: ':lib', configuration: 'testFixtures')
}
```
**Kotlin:**
```kotlin
dependencies {
implementation(project(":common"))
implementation(project(path = ":lib", configuration = "testFixtures"))
// With type-safe accessors (requires TYPESAFE_PROJECT_ACCESSORS)
implementation(projects.common)
}
```
#### File Dependencies
**Groovy:**
```groovy
dependencies {
implementation files('libs/custom.jar')
implementation fileTree(dir: 'libs', include: '*.jar')
}
```
**Kotlin:**
```kotlin
dependencies {
implementation(files("libs/custom.jar"))
implementation(fileTree("libs") { include("*.jar") })
}
```
#### Configuration-Specific Dependencies
**Groovy:**
```groovy
configurations {
integrationTestImplementation.extendsFrom testImplementation
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
}
dependencies {
integrationTestImplementation 'org.testcontainers:junit-jupiter:1.19.3'
}
```
**Kotlin:**
```kotlin
val integrationTestImplementation by configurations.creating {
extendsFrom(configurations.testImplementation.get())
}
val integrationTestRuntimeOnly by configurations.creating {
extendsFrom(configurations.testRuntimeOnly.get())
}
dependencies {
integrationTestImplementation("org.testcontainers:junit-jupiter:1.19.3")
}
```
### Configuration Block Conversions
#### allprojects and subprojects
**Groovy:**
```groovy
allprojects {
group = 'com.example'
version = '1.0.0'
repositories {
mavenCentral()
}
}
subprojects {
apply plugin: 'java'
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
}
}
```
**Kotlin:**
```kotlin
allprojects {
group = "com.example"
version = "1.0.0"
repositories {
mavenCentral()
}
}
subprojects {
apply(plugin = "java")
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
}
}
```
#### buildscript Block (Avoid in Modern Gradle)
**Groovy:**
```groovy
buildscript {
repositories {
gradlePluginPortal()
mavenCentral()
}
dependencies {
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.0'
}
}
```
**Kotlin (old way):**
```kotlin
buildscript {
repositories {
gradlePluginPortal()
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.0")
}
}
```
**Kotlin (modern way - use plugins block instead):**
```kotlin
plugins {
kotlin("jvm") version "2.0.0"
}
// No buildscript needed!
```
#### Task Configuration
**Groovy:**
```groovy
task myTask {
doLast {
println 'Task executed'
}
}
tasks.withType(Test) {
useJUnitPlatform()
}
tasks.named('build') {
dependsOn 'myTask'
}
```
**Kotlin:**
```kotlin
tasks.register("myTask") {
doLast {
println("Task executed")
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
tasks.named("build") {
dependsOn("myTask")
}
```
#### Java Configuration
**Groovy:**
```groovy
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
withSourcesJar()
withJavadocJar()
}
compileJava {
options.encoding = 'UTF-8'
}
```
**Kotlin:**
```kotlin
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
withSourcesJar()
withJavadocJar()
}
tasks.named<JavaCompile>("compileJava") {
options.encoding = "UTF-8"
}
```
#### Publishing Configuration
**Groovy:**
```groovy
publishing {
publications {
maven(MavenPublication) {
from components.java
groupId = 'com.example'
artifactId = 'my-library'
version = '1.0.0'
}
}
repositories {
maven {
url = 'https://repo.example.com/maven'
credentials {
username = project.findProperty('repoUser')
password = project.findProperty('repoPassword')
}
}
}
}
```
**Kotlin:**
```kotlin
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
groupId = "com.example"
artifactId = "my-library"
version = "1.0.0"
}
}
repositories {
maven {
url = uri("https://repo.example.com/maven")
credentials {
username = providers.gradleProperty("repoUser").orNull
password = providers.gradleProperty("repoPassword").orNull
}
}
}
}
```
#### Testing Configuration
**Groovy:**
```groovy
test {
useJUnitPlatform()
testLogging {
events 'passed', 'skipped', 'failed'
exceptionFormat 'full'
}
maxParallelForks = Runtime.runtime.availableProcessors()
}
```
**Kotlin:**
```kotlin
tasks.named<Test>("test") {
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
}
maxParallelForks = Runtime.getRuntime().availableProcessors()
}
```
#### Extra Properties
**Groovy:**
```groovy
ext {
springBootVersion = '3.2.0'
junitVersion = '5.10.2'
}
ext.kotlinVersion = '2.0.0'
dependencies {
implementation "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion"
}
```
**Kotlin:**
```kotlin
// Option 1: Using extra properties
extra["springBootVersion"] = "3.2.0"
extra["junitVersion"] = "5.10.2"
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web:${extra["springBootVersion"]}")
testImplementation("org.junit.jupiter:junit-jupiter:${extra["junitVersion"]}")
}
// Option 2: Type-safe delegates (recommended)
val springBootVersion by extra("3.2.0")
val junitVersion by extra("5.10.2")
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web:$springBootVersion")
testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion")
}
// Option 3: Regular Kotlin variables (best for build script only)
val springBootVersion = "3.2.0"
val junitVersion = "5.10.2"
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web:$springBootVersion")
testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion")
}
```
### Common Migration Gotchas
#### 1. String Quotes - Single vs Double
**Problem:**
```kotlin
// ERROR: Single quotes don't work in Kotlin
implementation('com.google.guava:guava:33.0.0-jre') // Compilation error!
```
**Solution:**
```kotlin
// Use double quotes in Kotlin
implementation("com.google.guava:guava:33.0.0-jre") // Correct
```
**Why:** Kotlin only supports double quotes for strings. Single quotes are for `Char` type.
#### 2. Method Call Parentheses
**Problem:**
```kotlin
// ERROR: Missing parentheses
implementation "com.google.guava:guava:33.0.0-jre" // Compilation error!
```
**Solution:**
```kotlin
// Always use parentheses for method calls
implementation("com.google.guava:guava:33.0.0-jre") // Correct
```
**Why:** Kotlin requires explicit parentheses for method calls (no implicit syntax like Groovy).
#### 3. Plugin ID Strings - kotlin vs "kotlin"
**Problem:**
```kotlin
plugins {
// ERROR: This doesn't work in Kotlin DSL
kotlin("jvm") version 2.0.0 // Compilation error - version is not a string!
}
```
**Solution:**
```kotlin
plugins {
// Correct: Version must be a string literal
kotlin("jvm") version "2.0.0" // Correct
// Alternative explicit form
id("org.jetbrains.kotlin.jvm") version "2.0.0" // Also correct
}
```
**Why:** The `version` infix function expects a string parameter, not an expression.
#### 4. Accessing Task by Name - Type Safety
**Problem:**
```kotlin
// Groovy way (not type-safe)
tasks.getByName("test") {
useJUnitPlatform() // No type information!
}
```
**Solution:**
```kotlin
// Kotlin way (type-safe)
tasks.named<Test>("test") {
useJUnitPlatform() // Type-safe! 'this' is Test
}
// Or using getByName with cast
tasks.getByName<Test>("test") {
useJUnitPlatform()
}
```
**Why:** Kotlin DSL encourages type-safe accessors for better IDE support and compile-time checks.
#### 5. Configuration Names with Hyphens
**Problem:**
```kotlin
plugins {
// ERROR: Hyphens in plugin names need backticks
java-library // Compilation error!
}
```
**Solution:**
```kotlin
plugins {
// Use backticks for kebab-case names
`java-library` // Correct
}
```
**Why:** Hyphens aren't valid in Kotlin identifiers, so backticks escape them.
#### 6. Assignment vs Method Calls in Configuration
**Problem:**
```kotlin
// Confusing when to use = vs method call
task {
description = "My task" // Property assignment
group("custom") // Method call?
}
```
**Solution:**
```kotlin
tasks.register("myTask") {
description = "My task" // Property assignment (setter)
group = "custom" // Also property assignment!
// Both work, but property syntax is more common in Kotlin
doLast {
println("Task executed")
}
}
```
**Why:** Kotlin has property syntax for getters/setters. Use `=` for properties, `()` for methods.
#### 7. Extra Properties Access
**Problem:**
```kotlin
// Groovy style (not type-safe)
ext.myVersion = "1.0"
println(ext.myVersion) // Error in Kotlin!
```
**Solution:**
```kotlin
// Option 1: Map-style access
extra["myVersion"] = "1.0"
println(extra["myVersion"])
// Option 2: Type-safe delegate (recommended)
val myVersion by extra("1.0")
println(myVersion)
// Option 3: Regular Kotlin variable (simplest)
val myVersion = "1.0"
println(myVersion)
```
**Why:** Kotlin DSL uses `extra` property with map-style or delegate access for type safety.
#### 8. Delegate Ambiguity in Configuration Blocks
**Problem:**
```kotlin
// Ambiguous delegate in nested blocks
repositories {
maven {
// Is 'url' from repository or project?
url = uri("https://example.com/maven") // Unclear!
}
}
```
**Solution:**
```kotlin
repositories {
maven {
// Explicitly use 'this' if ambiguous
this.url = uri("https://example.com/maven")
// Or use the receiver parameter name
url = uri("https://example.com/maven") // Usually clear from context
}
}
```
**Why:** Kotlin DSL sometimes requires explicit receiver (`this`) to disambiguate nested scopes.
#### 9. String Interpolation in Configuration
**Problem:**
```kotlin
// Variable not interpolated correctly
val myVersion = "1.0"
dependencies {
implementation("com.example:lib:$myVersion") // OK
implementation("com.example:lib:${project.version}") // project not in scope!
}
```
**Solution:**
```kotlin
val myVersion = "1.0"
val projectVersion = project.version // Capture outside if needed
dependencies {
implementation("com.example:lib:$myVersion") // OK
implementation("com.example:lib:$projectVersion") // OK
}
```
**Why:** Be aware of variable scope in configuration blocks. Capture values early if needed.
#### 10. Dynamic Properties
**Problem:**
```kotlin
// Groovy's dynamic properties don't work
project.myCustomProperty = "value" // Error in Kotlin!
println(project.myCustomProperty) // Error!
```
**Solution:**
```kotlin
// Use extra properties
extra["myCustomProperty"] = "value"
println(extra["myCustomProperty"])
// Or define extensions properly
abstract class MyExtension {
abstract val myProperty: Property<String>
}
val myExt = extensions.create<MyExtension>("myExt")
myExt.myProperty.set("value")
```
**Why:** Kotlin is statically typed; use `extra` for dynamic properties or define proper extensions.
#### 11. Task Container Configuration
**Problem:**
```kotlin
// Groovy uses 'all' without explicit call
tasks.withType(Test) { // Error in Kotlin
useJUnitPlatform()
}
```
**Solution:**
```kotlin
// Kotlin requires type parameter and explicit methods
tasks.withType<Test> {
useJUnitPlatform()
}
// Or with configureEach (lazy)
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
```
**Why:** Kotlin DSL uses generic type parameters (`<Test>`) for type safety.
#### 12. Creating vs Registering Tasks
**Problem:**
```kotlin
// Old Groovy pattern (eager)
tasks.create("myTask") {
doLast { println("Task") }
}
```
**Solution:**
```kotlin
// Modern Kotlin pattern (lazy)
tasks.register("myTask") {
doLast { println("Task") }
}
```
**Why:** `register` is lazy (better for configuration cache), `create` is eager. Prefer `register` in modern Gradle.
#### Migration Checklist
When migrating from Groovy to Kotlin DSL:
- [ ] Change file extension: `.gradle` → `.gradle.kts`
- [ ] Replace single quotes with double quotes
- [ ] Add parentheses to all method calls
- [ ] Add type parameters where needed (`<Test>`, `<MavenPublication>`)
- [ ] Use backticks for kebab-case identifiers
- [ ] Replace `ext` with `extra` or delegates
- [ ] Use `tasks.register` instead of `tasks.create`
- [ ] Use `tasks.named<Type>` instead of `tasks.getByName`
- [ ] Replace `project.property` with `providers.gradleProperty`
- [ ] Test with configuration cache enabled
#### Automated Migration Tools
While manual migration is often best, these tools can help:
```bash
# IntelliJ IDEA has built-in Groovy → Kotlin conversion
# Right-click build.gradle → Convert Groovy to Kotlin
# Gradle also provides a migration guide
# https://docs.gradle.org/current/userguide/migrating_from_groovy_to_kotlin_dsl.html
```
---
## Recommended Tooling
| Tool | Purpose |
|------|---------|
| `gradle wrapper` | Use Gradle Wrapper for version consistency |
| `gradle init` | Initialize new projects with proper structure |
| `gradle build --scan` | Build scans for performance analysis |
| `gradle --configuration-cache` | Enable configuration cache for faster builds |
| `gradle --build-cache` | Enable build cache for incremental builds |
---
## References
- Gradle Official Documentation: https://docs.gradle.org/
- Gradle Kotlin DSL Primer: https://docs.gradle.org/current/userguide/kotlin_dsl.html
- Gradle Best Practices: https://docs.gradle.org/current/userguide/authoring_maintainable_build_scripts.html
- Configuration Cache: https://docs.gradle.org/current/userguide/configuration_cache.html
- Build Cache: https://docs.gradle.org/current/userguide/build_cache.html