| 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
- Declarative over Imperative: Prefer declarative configuration that describes what you want, not how to achieve it
- Type-Safe Configuration: Use Kotlin DSL for type safety and IDE support
- Lazy Configuration: Use Providers API to defer configuration until needed
- Build Cache Friendly: Write tasks that support build caching for faster builds
- 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:
plugins {
java
application
id("com.github.johnrengelman.shadow") version "8.1.1"
}
group = "com.example"
version = "1.0.0"
repositories {
mavenCentral()
}
dependencies {
implementation("com.google.guava:guava:33.0.0-jre")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
tasks {
test {
useJUnitPlatform()
}
jar {
manifest {
attributes("Main-Class" to "com.example.Main")
}
}
}
settings.gradle.kts Basics
rootProject.name = "my-project"
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
include("app")
include("lib")
include("common")
project(":app").projectDir = file("applications/app")
Repository Configuration
repositories {
mavenCentral()
google()
maven {
name = "CompanyRepo"
url = uri("https://repo.company.com/maven")
credentials {
username = providers.gradleProperty("repoUser").orNull
password = providers.gradleProperty("repoPassword").orNull
}
}
}
Script Organization Best Practices
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")
}
fun configureJavaToolchain() {
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
vendor = JvmVendorSpec.ADOPTIUM
}
}
}
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:
- Initialization Phase - Determines which projects participate in the build
- Configuration Phase - Configures all projects and builds the task graph
- 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:
rootProject.name = "my-project"
println("Initialization phase")
include("app")
include("lib")
include("common")
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:
plugins {
java
}
version = "1.0.0"
println("Configuration phase")
tasks.register("myTask") {
group = "custom"
description = "Example task"
println("Task configuration")
doLast {
println("Task execution")
}
}
val projectVersion = version
println("Project version: $projectVersion")
val sourceFiles: Provider<FileTree> = providers.provider {
fileTree("src")
}
Duration: Can be slow if not careful (seconds to minutes for large projects)
Key Point: Configuration runs on every build, even if no tasks execute. Keep it fast!
3. Execution Phase
Purpose: Execute the selected tasks in dependency order.
What runs: Task actions (doFirst, doLast, @TaskAction)
What happens:
- Tasks execute in correct dependency order
- Task inputs are read
- Task outputs are generated
- Build artifacts are created
Example:
tasks.register("myTask") {
group = "custom"
doFirst {
println("Starting task")
}
doLast {
println("Task completed")
}
}
abstract class BuildTask : DefaultTask() {
@get:InputDirectory
abstract val sourceDir: DirectoryProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun build() {
println("Building...")
}
}
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
tasks.register("badTask") {
val files = File("src").listFiles()
println("Found ${files?.size} files")
doLast {
println("Processing ${files?.size} files")
}
}
tasks.register("goodTask") {
doLast {
val files = File("src").listFiles()
println("Found ${files?.size} files")
println("Processing ${files.size} files")
}
}
tasks.register("badConsumer") {
val compileOutput = tasks.named("compileJava").get().outputs.files
doLast {
println(compileOutput)
}
}
tasks.register("goodConsumer") {
val compileOutput = tasks.named("compileJava").map { it.outputs.files }
doLast {
println(compileOutput.get())
}
}
tasks.register("badFetch") {
val response = URL("https://api.example.com/version").readText()
doLast {
println("Version: $response")
}
}
tasks.register("goodFetch") {
val response: Provider<String> = providers.provider {
URL("https://api.example.com/version").readText()
}
doLast {
println("Version: ${response.get()}")
}
}
tasks.register("badProvider") {
val version = providers.gradleProperty("version").get()
doLast {
println("Version: $version")
}
}
tasks.register("goodProvider") {
val version = providers.gradleProperty("version")
doLast {
println("Version: ${version.get()}")
}
}
var counter = 0
tasks.register("bad1") {
counter++
doLast { println("Counter: $counter") }
}
tasks.register("bad2") {
counter++
doLast { println("Counter: $counter") }
}
Why Build Phases Matter
1. Build Performance
Configuration phase runs on every build:
./gradlew tasks
./gradlew clean
./gradlew build
./gradlew --stop
Slow configuration = slow every command, even ./gradlew tasks!
2. Configuration Cache
Configuration cache stores the result of configuration phase:
./gradlew build --configuration-cache
./gradlew clean build --configuration-cache
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
./gradlew build --profile
./gradlew build --configuration-cache --configuration-cache-problems=warn
./gradlew build --info | grep "Configuration"
./gradlew build --configuration-cache
./gradlew clean build --configuration-cache
Example: Full Build Lifecycle
println("1. Initialization phase: settings.gradle.kts")
rootProject.name = "lifecycle-demo"
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")
Dependency Management
Dependency Configurations
dependencies {
implementation("com.google.guava:guava:33.0.0-jre")
api("org.apache.commons:commons-lang3:3.14.0")
compileOnly("org.projectlombok:lombok:1.18.30")
runtimeOnly("com.h2database:h2:2.2.224")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
testImplementation("org.mockito:mockito-core:5.10.0")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
Version Catalogs (Modern Gradle Approach)
gradle/libs.versions.toml:
[versions]
guava = "33.0.0-jre"
junit = "5.10.2"
mockito = "5.10.0"
kotlin = "2.0.0"
[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" }
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
[bundles]
testing = ["junit-jupiter", "mockito-core"]
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
shadow = { id = "com.github.johnrengelman.shadow", version = "8.1.1" }
build.gradle.kts:
plugins {
alias(libs.plugins.kotlin.jvm)
}
dependencies {
implementation(libs.guava)
testImplementation(libs.bundles.testing)
testRuntimeOnly(libs.junit.platform.launcher)
}
Dependency Constraints
dependencies {
implementation("com.example:library:1.0")
constraints {
implementation("org.slf4j:slf4j-api:2.0.9") {
because("Earlier versions have security vulnerabilities")
}
}
constraints {
implementation("org.springframework.boot:spring-boot-starter-web:3.2.0")
implementation("org.springframework.boot:spring-boot-starter-data-jpa:3.2.0")
}
}
Platform/BOM Dependencies
dependencies {
implementation(platform("org.springframework.boot:spring-boot-dependencies:3.2.0"))
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
testImplementation(platform("org.junit:junit-bom:5.10.2"))
testImplementation("org.junit.jupiter:junit-jupiter")
}
Excluding Transitive Dependencies
dependencies {
implementation("com.example:library:1.0") {
exclude(group = "commons-logging", module = "commons-logging")
}
implementation("com.example:utility:1.0") {
isTransitive = false
}
implementation("org.slf4j:jcl-over-slf4j:2.0.9")
}
configurations.all {
exclude(group = "commons-logging", module = "commons-logging")
}
Dependency Notation
dependencies {
implementation("com.google.guava:guava:33.0.0-jre")
implementation(group = "com.google.guava", name = "guava", version = "33.0.0-jre")
implementation("net.java.dev.jna:jna:5.13.0:jpms")
implementation(files("libs/custom-library.jar"))
implementation(fileTree("libs") { include("*.jar") })
implementation(project(":common"))
}
Plugin Configuration
Plugin Application
plugins {
java
application
id("com.github.johnrengelman.shadow") version "8.1.1"
kotlin("jvm") version "2.0.0"
id("org.springframework.boot") version "3.2.0" apply false
}
Using Version Catalogs with Plugins
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.shadow)
}
Common Plugins
Java Plugin:
plugins {
java
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
vendor = JvmVendorSpec.ADOPTIUM
}
modularity.inferModulePath = true
withSourcesJar()
withJavadocJar()
}
Kotlin JVM Plugin:
plugins {
kotlin("jvm") version "2.0.0"
}
kotlin {
jvmToolchain(21)
explicitApi()
compilerOptions {
freeCompilerArgs.add("-Xjsr305=strict")
allWarningsAsErrors = true
}
}
Application Plugin:
plugins {
application
}
application {
mainClass = "com.example.Main"
applicationName = "my-app"
applicationDefaultJvmArgs = listOf("-Xmx512m", "-Xms256m")
}
Java Library Plugin:
plugins {
`java-library`
}
dependencies {
api("org.apache.commons:commons-lang3:3.14.0")
implementation("com.google.guava:guava:33.0.0-jre")
}
Configuring Plugin Extensions
plugins {
java
jacoco
}
jacoco {
toolVersion = "0.8.11"
reportsDirectory = layout.buildDirectory.dir("reports/jacoco")
}
tasks.jacocoTestReport {
dependsOn(tasks.test)
reports {
xml.required = true
html.required = true
csv.required = false
}
}
Conditional Plugin Application
plugins {
java
if (project.hasProperty("enableKotlin")) {
kotlin("jvm") version "2.0.0"
}
}
if (project.findProperty("coverage") == "true") {
apply(plugin = "jacoco")
}
Multi-Module Projects
Project Structure
my-project/
├── settings.gradle.kts # Project structure definition
├── build.gradle.kts # Root build script
├── gradle/
│ └── libs.versions.toml # Shared version catalog
├── app/
│ ├── build.gradle.kts # Application module
│ └── src/
├── lib/
│ ├── build.gradle.kts # Library module
│ └── src/
└── common/
├── build.gradle.kts # Shared code module
└── src/
settings.gradle.kts:
rootProject.name = "my-project"
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
include("app")
include("lib")
include("common")
include("backend:api")
include("backend:service")
Root build.gradle.kts:
plugins {
java apply false
kotlin("jvm") version "2.0.0" apply false
}
allprojects {
group = "com.example"
version = "1.0.0"
repositories {
mavenCentral()
}
}
subprojects {
}
Convention Plugins (Recommended Approach)
Convention plugins encapsulate shared configuration in a type-safe, reusable way.
buildSrc/build.gradle.kts:
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
}
buildSrc/src/main/kotlin/java-conventions.gradle.kts:
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:
plugins {
id("java-conventions")
application
}
application {
mainClass = "com.example.app.Main"
}
dependencies {
implementation(project(":lib"))
implementation(project(":common"))
}
lib/build.gradle.kts:
plugins {
id("java-conventions")
`java-library`
}
dependencies {
api(project(":common"))
implementation("com.google.guava:guava:33.0.0-jre")
}
Cross-Module Dependencies
dependencies {
implementation(projects.common)
implementation(projects.backend.api)
implementation(project(":common"))
implementation(project(":backend:api"))
testImplementation(project(path = ":lib", configuration = "testFixtures"))
}
Shared Configuration Patterns
Pattern 1: subprojects {} (Quick but limited)
subprojects {
apply(plugin = "java")
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
}
}
Pattern 2: Convention Plugins (Recommended)
plugins {
`java-library`
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
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:
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:
includeBuild("../other-project")
Dependency Management Across Modules
Using Version Catalogs (Recommended):
[versions]
guava = "33.0.0-jre"
[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
Platform Projects (Alternative):
plugins {
`java-platform`
}
dependencies {
constraints {
api("com.google.guava:guava:33.0.0-jre")
api("org.slf4j:slf4j-api:2.0.9")
}
}
dependencies {
implementation(platform(project(":platform")))
implementation("com.google.guava:guava")
}
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:
org.gradle.configuration-cache=true
Or via command line:
./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:
val myProperty: Provider<String> = providers.gradleProperty("myProp")
tasks.register("example") {
doLast {
println(myProperty.get())
}
}
Build Cache (Enhanced in Gradle 9)
Enable in gradle.properties:
org.gradle.caching=true
Or via command line:
./gradlew build --build-cache
Configure cache:
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"
credentials {
username = providers.gradleProperty("cacheUser").orNull
password = providers.gradleProperty("cachePassword").orNull
}
}
}
Improved Test Suites API
testing {
suites {
val test by getting(JvmTestSuite::class) {
useJUnitJupiter("5.10.2")
}
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)
}
}
}
}
}
}
Java Toolchains (Enhanced)
java {
toolchain {
vendor = JvmVendorSpec.ADOPTIUM
languageVersion = JavaLanguageVersion.of(21)
}
}
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:
Isolated Projects (Experimental in Gradle 9)
Parallel configuration of subprojects for massive multi-module builds.
# 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
Performance Improvements in Gradle 9
- Faster dependency resolution - Up to 40% faster for large dependency graphs
- Improved incremental compilation - Better change detection for Java/Kotlin
- Enhanced file system watching - More efficient change detection
- Better daemon memory management - Reduced memory usage over time
- Optimized configuration cache - Faster serialization/deserialization
Best Practices for Gradle 9
org.gradle.caching=true
org.gradle.configuration-cache=true
org.gradle.parallel=true
org.gradle.vfs.watch=true
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
tasks.register("myTask") {
doLast { ... }
}
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
tasks.create("eagerTask") {
doLast {
println("Task executed")
}
}
tasks.register("lazyTask") {
doLast {
println("Task executed")
}
}
Task Actions
tasks.register("hello") {
doLast {
println("Hello from task")
}
}
tasks.register("multiAction") {
doFirst {
println("First action")
}
doLast {
println("Last action")
}
}
tasks.register("namedAction") {
val myAction = Action<Task> {
println("Named action")
}
doLast(myAction)
}
Abstract Task Classes (Recommended for Reusable Tasks)
import org.gradle.api.DefaultTask
import org.gradle.api.file.*
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
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 {
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")
}
}
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:
abstract class AdvancedTask : DefaultTask() {
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
abstract val inputFile: RegularFileProperty
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val inputFiles: ConfigurableFileCollection
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val inputDir: DirectoryProperty
@get:Input
abstract val message: Property<String>
@get:Input
@get:Optional
abstract val optionalFlag: Property<Boolean>
@get:OutputFile
abstract val outputFile: RegularFileProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@get:Internal
abstract val internalState: Property<String>
@TaskAction
fun execute() {
}
}
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
tasks.register("taskA") {
doLast { println("Task A") }
}
tasks.register("taskB") {
dependsOn("taskA")
doLast { println("Task B") }
}
tasks.register("taskC") {
dependsOn("taskA", "taskB")
doLast { println("Task C") }
}
tasks.register("taskD") {
mustRunAfter("taskB")
doLast { println("Task D") }
}
tasks.register("taskE") {
shouldRunAfter("taskD")
doLast { println("Task E") }
}
tasks.register("taskF") {
doLast { println("Task F") }
}
tasks.register("cleanup") {
doLast { println("Cleanup") }
}
tasks.named("taskF") {
finalizedBy("cleanup")
}
Working Example: File Processing Task
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")
}
}
tasks.register<TransformMarkdownTask>("transformMarkdown") {
markdownFiles.from(fileTree("docs") { include("**/*.md") })
htmlOutputDir = layout.buildDirectory.dir("html")
title = "My Project Documentation"
}
Task Configuration Avoidance
tasks.named<JavaCompile>("compileJava") {
options.compilerArgs.add("-Xlint:unchecked")
}
val compileJavaTask = tasks.named("compileJava")
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
abstract class GreetingExtension {
abstract val message: Property<String>
abstract val times: Property<Int>
init {
message.convention("Hello")
times.convention(1)
}
}
class GreetingPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create("greeting", GreetingExtension::class.java)
project.tasks.register("greet") {
doLast {
repeat(extension.times.get()) {
println(extension.message.get())
}
}
}
}
}
plugins {
id("greeting-plugin")
}
greeting {
message = "Hello, Gradle!"
times = 3
}
Extension Anti-Patterns
abstract class BadExtension {
var message: String = "Hello"
var times: Int = 1
}
abstract class GoodExtension {
abstract val message: Property<String>
abstract val times: Property<Int>
}
class BadPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create("bad", BadExtension::class.java)
val msg = extension.message.get()
project.tasks.register("bad") {
doLast { println(msg) }
}
}
}
class GoodPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create("good", GoodExtension::class.java)
project.tasks.register("good") {
doLast {
println(extension.message.get())
}
}
}
}
abstract class ExtensionWithoutDefaults {
abstract val required: Property<String>
}
abstract class ExtensionWithDefaults {
abstract val optional: Property<String>
init {
optional.convention("sensible-default")
}
}
Extension with Nested Configuration
abstract class DatabaseExtension {
abstract val host: Property<String>
abstract val port: Property<Int>
abstract val username: Property<String>
abstract val password: Property<String>
}
abstract class AppExtension(objects: ObjectFactory) {
abstract val appName: Property<String>
abstract val version: Property<String>
val database: DatabaseExtension = objects.newInstance(DatabaseExtension::class.java)
fun database(action: Action<DatabaseExtension>) {
action.execute(database)
}
init {
appName.convention("MyApp")
version.convention("1.0.0")
database.port.convention(5432)
}
}
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:
import org.gradle.api.NamedDomainObjectContainer
abstract class ServerConfig(val name: String) {
abstract val host: Property<String>
abstract val port: Property<Int>
init {
port.convention(8080)
}
}
abstract class DeploymentExtension(objects: ObjectFactory) {
val servers: NamedDomainObjectContainer<ServerConfig> =
objects.domainObjectContainer(ServerConfig::class.java)
fun servers(action: Action<NamedDomainObjectContainer<ServerConfig>>) {
action.execute(servers)
}
}
deployment {
servers {
create("production") {
host = "prod.example.com"
port = 443
}
create("staging") {
host = "staging.example.com"
port = 8080
}
}
}
tasks.register("deployToProduction") {
doLast {
val prodServer = extensions.getByType<DeploymentExtension>()
.servers.getByName("production")
println("Deploying to ${prodServer.host.get()}:${prodServer.port.get()}")
}
}
Extension Best Practices
abstract class WellDesignedExtension @Inject constructor(
private val objects: ObjectFactory,
private val providers: ProviderFactory
) {
abstract val apiKey: Property<String>
abstract val timeout: Property<Int>
val apiUrl: Provider<String> = apiKey.map { key ->
"https://api.example.com?key=$key"
}
init {
timeout.convention(30)
apiKey.finalizeValueOnRead()
}
fun useDefaultCredentials() {
apiKey.set(providers.environmentVariable("API_KEY"))
}
fun useCustomCredentials(key: String) {
apiKey.set(key)
}
}
Connecting Extension to Tasks
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") {
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 Basics
val messageProvider: Provider<String> = providers.provider {
"Message computed at ${System.currentTimeMillis()}"
}
tasks.register("printMessage") {
doLast {
println(messageProvider.get())
}
}
val apiKeyProvider: Provider<String> = providers.environmentVariable("API_KEY")
val debugProvider: Provider<String> = providers.systemProperty("debug")
val versionProvider: Provider<String> = providers.gradleProperty("app.version")
Property for Mutable Values
abstract class ConfigurableTask : DefaultTask() {
@get:Input
abstract val message: Property<String>
@get:Input
abstract val count: Property<Int>
init {
message.convention("Default message")
count.convention(1)
}
@TaskAction
fun execute() {
repeat(count.get()) {
println(message.get())
}
}
}
tasks.register<ConfigurableTask>("configurable") {
message.set("Hello from property")
count.set(5)
}
Transforming Providers
val version: Provider<String> = providers.gradleProperty("version")
val fullVersion: Provider<String> = version.map { v ->
"v$v-${System.currentTimeMillis()}"
}
val baseUrl: Provider<String> = providers.gradleProperty("baseUrl")
val apiUrl: Provider<String> = baseUrl.flatMap { base ->
providers.provider { "$base/api/v1" }
}
val timeout: Provider<Int> = providers.gradleProperty("timeout")
.map { it.toInt() }
.orElse(30)
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
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()}")
}
}
val sourceTask = tasks.register<SourceTask>("source")
tasks.register<TargetTask>("target") {
targetMessage.set(sourceTask.flatMap { it.sourceMessage })
}
File and Directory Providers
abstract class FileTask : DefaultTask() {
@get:OutputFile
abstract val outputFile: RegularFileProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun execute() {
val file = outputFile.get().asFile
val dir = outputDir.get().asFile
file.writeText("Output content")
println("Wrote to ${file.absolutePath}")
}
}
tasks.register<FileTask>("fileTask") {
outputFile.set(layout.buildDirectory.file("output.txt"))
outputDir.set(layout.buildDirectory.dir("outputs"))
}
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
abstract class CollectionTask : DefaultTask() {
@get:Input
abstract val items: ListProperty<String>
@get:Input
abstract val tags: SetProperty<String>
@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") {
items.set(listOf("a", "b", "c"))
items.add("d")
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
val version: Provider<String> = providers.gradleProperty("version")
tasks.register("good") {
doLast {
println(messageProvider.get())
}
}
abstract class GoodTask : DefaultTask() {
@get:Input
abstract val myProperty: Property<String>
@TaskAction
fun execute() {
println(myProperty.get())
}
}
Provider Best Practices
val externalConfig: Provider<String> = providers.fileContents(
layout.projectDirectory.file("config.txt")
).asText
val criticalValue: Property<String> = objects.property(String::class.java)
criticalValue.finalizeValueOnRead()
val timeout: Property<Int> = objects.property(Int::class.java)
timeout.convention(30)
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
}
val expensiveValue: Provider<String> = providers.provider {
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):
org.gradle.caching=true
Or via command line:
./gradlew build --build-cache
How it works:
- Gradle calculates cache key from task inputs
- If cache hit: Reuses outputs, task shows "FROM-CACHE"
- If cache miss: Executes task, stores outputs
Writing Cache-Compatible Tasks
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.*
@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())
}
}
}
}
abstract class BadTask : DefaultTask() {
@TaskAction
fun execute() {
val timestamp = System.currentTimeMillis()
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
tasks.withType<Test>().configureEach {
outputs.cacheIf { true }
}
normalization {
runtimeClasspath {
ignore("META-INF/MANIFEST.MF")
}
}
Configuration Cache
Configuration cache stores the configured task graph, eliminating configuration phase on subsequent builds.
Enable configuration cache (gradle.properties):
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn # Or 'fail'
Or via command line:
./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
tasks.register("compatible") {
val message: Provider<String> = providers.gradleProperty("message")
doLast {
println(message.get())
}
}
interface MyBuildService : BuildService<BuildServiceParameters.None> {
fun performWork() {
println("Build service performing work")
}
}
abstract class SharedStateTask : DefaultTask() {
@get:Internal
abstract val myService: Property<MyBuildService>
@TaskAction
fun execute() {
myService.get().performWork()
}
}
val myServiceProvider = gradle.sharedServices.registerIfAbsent("myService", MyBuildService::class) {
}
tasks.register<SharedStateTask>("taskWithService") {
myService.set(myServiceProvider)
}
object BadSharedState {
var counter = 0
}
tasks.register("badProducer") {
doLast {
File("shared.txt").writeText("data")
}
}
tasks.register("badConsumer") {
doLast {
val data = File("shared.txt").readText()
println(data)
}
}
Common Configuration Cache Issues
tasks.register("good") {
val projectName = project.name
doLast {
println(projectName)
}
}
abstract class GoodTask : DefaultTask() {
@get:OutputFile
abstract val outputFile: RegularFileProperty
@TaskAction
fun execute() {
outputFile.get().asFile.appendText("item\n")
}
}
Cache Debugging
./gradlew build --build-cache --info | grep "Caching disabled"
./gradlew help --task processFiles
rm -rf ~/.gradle/caches/build-cache-*
rm -rf .gradle/build-cache
./gradlew build --configuration-cache --configuration-cache-problems=warn
./gradlew clean build --no-build-cache --no-configuration-cache
./gradlew clean build --build-cache --configuration-cache
Remote Build Cache
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
}
}
}
Cache Performance Tips
abstract class OptimizedTask : DefaultTask() {
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val sources: ConfigurableFileCollection
}
normalization {
runtimeClasspath {
ignore("**/*.txt")
ignore("META-INF/MANIFEST.MF")
}
}
val sources: ConfigurableFileCollection = objects.fileCollection()
sources.from(fileTree("src") { include("**/*.java") })
tasks.register("compileAll") {
dependsOn("compileModule1", "compileModule2", "compileModule3")
}
Measuring Cache Effectiveness
./gradlew build --scan
Custom Plugins
Custom plugins encapsulate build logic for reuse across projects or modules.
Binary Plugin (Plugin)
import org.gradle.api.Plugin
import org.gradle.api.Project
class GreetingPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create(
"greeting",
GreetingExtension::class.java
)
project.tasks.register("greet") {
group = "custom"
description = "Prints a greeting message"
doLast {
repeat(extension.times.get()) {
println(extension.message.get())
}
}
}
}
}
abstract class GreetingExtension {
abstract val message: Property<String>
abstract val times: Property<Int>
init {
message.convention("Hello from plugin")
times.convention(1)
}
}
implementation-class=GreetingPlugin
Precompiled Script Plugin (Recommended for Simple Plugins)
Easier approach using Kotlin DSL directly:
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"])
}
}
}
Complete Plugin Example
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) {
val extension = project.extensions.create(
"documentation",
DocumentationExtension::class.java
)
extension.sourceDir.convention(
project.layout.projectDirectory.dir("docs")
)
extension.outputDir.convention(
project.layout.buildDirectory.dir("docs")
)
project.tasks.register<GenerateDocsTask>("generateDocs") {
sourceDir.set(extension.sourceDir)
outputDir.set(extension.outputDir)
format.set(extension.format)
group = "documentation"
description = "Generates documentation"
}
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:
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) {
val metricsService = project.gradle.sharedServices.registerIfAbsent(
"metrics",
MetricsService::class.java
) {}
project.tasks.register<MetricsTask>("recordMetrics") {
this.metricsService.set(metricsService)
}
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
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
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")
}
}
}
Plugin Best Practices
class WellDesignedPlugin : Plugin<Project> {
override fun apply(project: Project) {
require(project.hasProperty("requiredProp")) {
"Plugin requires 'requiredProp' property"
}
val extension = project.extensions.create("wellDesigned", Extension::class.java)
project.tasks.register("myTask") {
}
project.pluginManager.apply("java")
project.plugins.withType<JavaPlugin> {
project.java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
}
project.afterEvaluate {
}
}
}
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:
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
implementation("com.github.johnrengelman:shadow:8.1.1")
}
buildSrc/settings.gradle.kts:
rootProject.name = "buildSrc"
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
mavenCentral()
gradlePluginPortal()
}
}
buildSrc/src/main/kotlin/java-conventions.gradle.kts:
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:
plugins {
id("java-conventions")
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:
rootProject.name = "my-project"
includeBuild("../build-logic")
include("app")
include("lib")
build-logic/settings.gradle.kts:
rootProject.name = "build-logic"
dependencyResolutionManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
build-logic/build.gradle.kts:
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:
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:
plugins {
`kotlin-dsl`
`maven-publish`
}
group = "com.example.gradle"
version = "1.0.0"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(11)
}
}
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:
pluginManagement {
repositories {
maven {
url = uri("https://repo.company.com/maven")
}
gradlePluginPortal()
}
}
Consumer build.gradle.kts:
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:
rootProject.name = "project-a"
includeBuild("../shared-library")
project-a/build.gradle.kts:
dependencies {
implementation("com.example:shared-library:1.0.0")
}
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").
plugins {
java
}
java {
toolchain.languageVersion = JavaLanguageVersion.of(21)
}