# Performance Optimization Standards for Gradle
This document outlines performance optimization standards for Gradle builds. It provides guidelines to ensure that builds are fast, efficient, and maintainable. Following these standards will reduce build times, improve developer productivity, and minimize resource consumption.
## 1. Project Structure and Configuration
### 1.1. Multi-Module Projects
**Do This:** Embrace multi-module projects for large codebases to enable parallel execution of tasks and incremental builds.
**Don't Do This:** Lump all source code into a single module, which can lead to long build times and hinder incremental compilation.
**Why:** Multi-module projects allow Gradle to parallelize task execution across modules, significantly reducing build times. Changes in one module do not necessarily trigger a rebuild of other modules, improving incremental build performance.
**Example:** Consider a project with backend, frontend, and common libraries:
"""gradle
// settings.gradle.kts
rootProject.name = "my-application"
include("backend")
include("frontend")
include("common")
"""
"""gradle
// backend/build.gradle.kts
plugins {
java
}
dependencies {
implementation(project(":common"))
// Other dependencies
}
"""
**Anti-Pattern:** A single, monolithic "build.gradle.kts" file that defines everything in one place.
### 1.2. Configuration Avoidance
**Do This:** Use configuration avoidance APIs to defer the evaluation of tasks and dependencies until they are strictly necessary.
**Don't Do This:** Eagerly configure tasks and dependencies, which can slow down build configuration time.
**Why:** Configuration avoidance reduces the overhead during the configuration phase by only evaluating what is needed when it is needed.
**Example:**
"""gradle
// build.gradle.kts
tasks.register("myTask") {
// Configuration avoided until the task is actually needed
doLast {
println("Running myTask")
}
}
"""
**Anti-Pattern:** Directly configuring tasks in the top-level "build.gradle.kts" without lazy evaluation, leading to unnecessary configuration overhead.
### 1.3. Using the Configuration Cache
**Do This:** Enable the configuration cache for faster build times, especially in CI environments.
**Don't Do This:** Ignore the configuration cache, which can lead to redundant configuration execution.
**Why:** The configuration cache stores the result of the configuration phase, allowing Gradle to skip this phase on subsequent builds.
**Example:**
"""gradle
// gradle.properties
org.gradle.configuration-cache=true
"""
**Important considerations for configuration cache:**
* Ensure tasks are compatible with configuration cache (all inputs/outputs are serializable).
* Use "@CacheableTask" annotation.
* Avoid using mutable state during configuration.
**Anti-Pattern:** Not enabling configuration cache or not addressing configuration cache incompatibilities.
## 2. Dependency Management
### 2.1. Version Alignment and Centralized Dependency Management
**Do This:** Use Gradle's version catalog feature to centralize dependency management and ensure consistent versions across all modules.
**Don't Do This:** Declare dependency versions directly in "build.gradle.kts" files, which can lead to version conflicts and inconsistencies.
**Why:** Version catalogs enable centralized dependency management and resolution, ensuring consistent versions across modules, which simplifies dependency upgrades and reduces the risk of conflicts.
**Example:**
"""toml
# gradle/libs.versions.toml
[versions]
guava = "31.1-jre"
[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
[bundles]
util = ["guava"]
"""
"""gradle
// build.gradle.kts
dependencies {
implementation(libs.guava)
// OR
implementation(libs.bundles.util) // for dependency bundles
}
"""
**Anti-Pattern:** Scattered and inconsistent dependency versions declared throughout multiple "build.gradle.kts" files.
### 2.2. Avoid Dynamic Versions
**Do This:** Specify fixed versions (e.g., "1.2.3") for dependencies.
**Don't Do This:** Use dynamic versions (e.g., "1.+", "latest.release").
**Why:** Dynamic versions can lead to unpredictable builds and potential compatibility issues. Fixed versions ensure reproducibility and stability.
**Example:**
"""gradle
dependencies {
implementation("com.example:library:1.2.3") // Correct: Fixed version
// Do NOT do: implementation("com.example:library:1.+")
}
"""
**Anti-Pattern:** Using "+" or "latest.release" for dependency versions, which can break builds unexpectedly.
### 2.3. Dependency Resolution Strategies
**Do This:** Configure dependency resolution strategies to handle version conflicts and force specific versions when necessary.
**Don't Do This:** Rely on default dependency resolution behavior, which can lead to unexpected version conflicts.
**Why:** Properly configured resolution strategies ensure that dependency versions are consistent and compatible.
**Example:**
"""gradle
configurations.all {
resolutionStrategy {
force("com.google.guava:guava:31.1-jre")
}
}
"""
**Anti-Pattern:** Ignoring dependency resolution conflicts, leading to runtime errors and unexpected behavior.
### 2.4. Dependency Locking
**Do This:** Use dependency locking to capture the exact versions of all dependencies used in a build.
**Don't Do This:** Omit dependency locking, resulting in potential version drift and inconsistent builds over time.
**Why:** Dependency locking ensures that builds are reproducible and immune to unexpected changes in dependency versions.
**Example:**
"""gradle
tasks.register("lockDependencies") {
doLast {
configurations.all {
resolutionStrategy.apply {
eachDependency {
if (requested.group == "org.springframework") {
useVersion("5.3.28") // example
}
}
}
resolutionStrategy.force("org.apache.commons:commons-lang3:3.12.0")
resolutionStrategy.failOnVersionConflict()
}
}
}
"""
Run this code in one gradle task and remove the task again. Add the created "*.lockfile" to your source control system.
"""gradle
dependencies {
implementation("org.springframework:spring-core")
implementation("org.apache.commons:commons-lang") // will fail
}
"""
gradle dependencies will show you the version conflict.
**Anti-Pattern:** Not using dependency locking, leading to potential version drift and inconsistent builds.
### 2.5. Caching Dependencies
**Do This:** Leverage Gradle's dependency caching mechanisms, including the build cache and HTTP caching proxies.
**Don't Do This:** Rely solely on remote repositories for every build, which can be slow and unreliable.
**Why:** Caching dependencies locally and in shared caches reduces the need to download dependencies from remote repositories, speeding up builds.
**Example:** Configure a local Maven repository as a cache:
"""gradle
repositories {
mavenLocal()
mavenCentral()
}
"""
For shared caching solutions, consider Gradle Enterprise build cache or solutions like Artifactory or Nexus.
**Anti-Pattern:** Not utilizing dependency caching, causing unnecessary downloads and slowing down builds.
## 3. Task Management and Optimization
### 3.1. Incremental Builds
**Do This:** Design tasks to be incremental by defining clear inputs and outputs.
**Don't Do This:** Create tasks that always run from scratch, even when inputs have not changed.
**Why:** Incremental tasks only re-execute when their inputs have changed, significantly reducing build times.
**Example:**
"""gradle
import org.gradle.api.tasks.*
@CacheableTask
abstract class MyTask : DefaultTask() {
@get:InputDirectory
abstract val inputDir: DirectoryProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun execute() {
// Process files in inputDir and write results to outputDir
}
}
"""
**Anti-Pattern:** Creating tasks without proper input/output declarations, negating incremental build benefits.
### 3.2. Task Dependencies
**Do This:** Define explicit task dependencies using "dependsOn" to ensure tasks are executed in the correct order.
**Don't Do This:** Rely on implicit task dependencies or task ordering, which can be unreliable and lead to build failures.
**Why:** Explicit task dependencies ensure that tasks are executed in the correct order, preventing errors and ensuring build integrity.
**Example:**
"""gradle
tasks.register("taskA") {
doLast {
println("Task A")
}
}
tasks.register("taskB") {
dependsOn("taskA")
doLast {
println("Task B")
}
}
"""
**Anti-Pattern:** Creating circular task dependencies, leading to build failures.
### 3.3. Parallel Execution
**Do This:** Enable parallel execution to run tasks concurrently, reducing overall build time.
**Don't Do This:** Disable parallel execution unless absolutely necessary, which can significantly increase build times.
**Why:** Parallel execution leverages multi-core processors to run tasks concurrently, significantly reducing overall build time.
**Example:**
"""gradle
// gradle.properties
org.gradle.parallel=true
"""
**Anti-Pattern:** Forcing sequential execution unnecessarily, hindering build performance on multi-core machines.
### 3.4. Task Input/Output Annotations
**Do This:** Use Gradle's task input/output annotations (e.g., "@Input", "@OutputDirectory", "@InputFile", "@InputChanges") to define task inputs and outputs clearly.
**Don't Do This:** Neglect to annotate task inputs and outputs, which prevents Gradle from performing incremental builds and caching tasks effectively.
**Why:** Proper input/output annotations enable Gradle to track changes to task inputs and outputs, allowing it to avoid re-executing tasks when nothing has changed.
**Example:**
"""gradle
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
@CacheableTask
abstract class MyTransformTask : DefaultTask() {
@get:Input
abstract val greeting: Property
@get:InputFile
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val inputFile: RegularFileProperty
@get:OutputFile
abstract val outputFile: RegularFileProperty
@TaskAction
fun transform() {
val input = inputFile.get().asFile.readText()
val output = "${greeting.get()}, ${input.uppercase()}"
outputFile.get().asFile.writeText(output)
}
}
"""
**Anti-Pattern:** Using "java.io.File" directly instead of "org.gradle.api.file.*" and appropriate "@Input..." annotations.
### 3.5. Using Gradle Worker API for Parallelism within Tasks
**Do This:** Use the Gradle Worker API to parallelize work within a single task, especially when processing large datasets.
**Don't Do This:** Perform all work sequentially within a task, which can limit performance and prevent leveraging multi-core processors.
**Why:** The Worker API allows Gradle to execute tasks in parallel using a managed worker process, improving CPU utilization and reducing task execution time. This works also in conjunction with the build cache.
**Example:**
"""gradle
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import org.gradle.workers.*
import javax.inject.Inject
@CacheableTask
abstract class MyParallelTask : DefaultTask() {
@get:Inject
abstract val workerExecutor: WorkerExecutor
@get:Input
abstract val message: Property
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val inputFiles: ConfigurableFileCollection
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun execute() {
inputFiles.files.forEach { inputFile ->
workerExecutor.noIsolation().submit(MyWorker::class.java) {
it.message.set(message)
it.inputFile.set(inputFile)
it.outputFile.set(outputDir.file(inputFile.name + ".out"))
}
}
}
interface MyParams : WorkParameters {
val message: Property
val inputFile: Property
val outputFile: Property
}
abstract class MyWorker @Inject constructor() : WorkAction {
override fun execute() {
val params = parameters
val input = params.inputFile.get().readText()
val output = "${params.message.get()}, ${input.uppercase()}"
params.outputFile.get().writeText(output)
}
}
}
"""
**Anti-Pattern:** Processing files sequentially in a task instead of using the Worker API.
## 4. Build Script Optimization
### 4.1. Optimize Build Script Code
**Do This:** Write efficient and concise build script code, avoiding unnecessary computations and logic.
**Don't Do This:** Write bloated and inefficient build scripts, which can slow down build configuration and execution.
**Why:** Well-optimized build scripts improve the overall performance and maintainability of the build process.
**Example:** Avoid complex logic in "build.gradle.kts" files. Move complex logic into dedicated classes or plugins.
**Anti-Pattern:** Complex and unreadable code in the "build.gradle.kts" file.
### 4.2. Enable Gradle Daemon
**Do This:** Enable the Gradle Daemon to keep Gradle running in the background, reducing startup time for subsequent builds.
**Don't Do This:** Disable the Gradle Daemon, which can increase build times, especially for frequent builds.
**Why:** The Gradle Daemon keeps Gradle running in the background, avoiding the overhead of JVM startup for each build.
**Example:**
"""gradle
# gradle.properties
org.gradle.daemon=true
"""
**Anti-Pattern:** Disabling the Gradle Daemon, causing unnecessary JVM startup overhead.
### 4.3. Profiling Your Build
**Do This:** Use Gradle's profiling capabilities to identify performance bottlenecks and optimize your build.
**Don't Do This:** Ignore performance issues and fail to optimize your build, leading to slow and inefficient builds.
**Why:** Profiling reveals performance bottlenecks, allowing you to focus your optimization efforts on the most critical areas.
**Example:**
"""bash
gradle build --profile
# OR
gradle build --scan
"""
The "--profile"/"--scan" options generate detailed reports that help identify performance bottlenecks.
**Anti-Pattern:** Not profiling builds or ignoring profiling results.
### 4.4. Keep Gradle Up-to-Date
**Do This:** Upgrade to the latest stable version of Gradle to take advantage of performance improvements and bug fixes.
**Don't Do This:** Use outdated versions of Gradle, which may contain performance issues and lack modern features.
**Why:** Newer versions of Gradle often include performance enhancements and bug fixes that can significantly improve build times. Check the Gradle release notes for details on performance improvements in each release.
**Example:** Upgrade Gradle using the Gradle wrapper:
"""bash
./gradlew wrapper --gradle-version 8.5 //Example Version
"""
**Anti-Pattern:** Using outdated Gradle versions, missing out on performance improvements.
## 5. Code Generation and Annotation Processing
### 5.1. Incremental Annotation Processing
**Do This:** Use incremental annotation processors to avoid re-processing unchanged files.
**Don't Do This:** Use non-incremental annotation processors, which can slow down build times unnecessarily.
**Why:** Incremental annotation processors only process changed files, significantly reducing build times in projects with heavy annotation processing.
**Example:** Use the "kapt.incremental.apt=true" option with Kotlin annotation processing.
**Anti-Pattern:** Using non-incremental annotation processors, leading to unnecessary processing overhead.
### 5.2. Code Generation Strategies
**Do This:** Optimize code generation processes to minimize the amount of generated code and reduce build times.
**Don't Do This:** Generate excessive amounts of code, which can increase build times and impact runtime performance.
**Why:** Efficient code generation reduces the amount of code that needs to be compiled, improving build times and runtime performance.
**Example:** Use efficient templates and algorithms for code generation.
**Anti-Pattern:** Generating excessive amounts of code unnecessarily.
## 6. Testing
### 6.1. Parallel Test Execution
**Do This:** Enable parallel test execution to run tests concurrently, reducing test execution time.
**Don't Do This:** Run tests sequentially, which can significantly increase test execution time, especially for large test suites.
**Why:** Parallel test execution leverages multi-core processors to run tests concurrently, reducing overall test execution time.
**Example:**
"""gradle
tasks.test {
maxParallelForks = (Runtime.runtime.availableProcessors() / 2).takeIf { it > 0 } ?: 1
}
"""
**Anti-Pattern:** Running tests sequentially, hindering test suite performance on multi-core machines.
### 6.2. Test Filtering
**Do This:** Use test filtering to run only relevant tests during development and CI.
**Don't Do This:** Run all tests every time, even when only a small part of the code has changed.
**Why:** Test filtering reduces the amount of time spent running tests, allowing developers to focus on the specific tests that are relevant to their changes.
**Example:**
"""bash
gradle test --tests "com.example.MyTest"
"""
**Anti-Pattern:** Running all tests unnecessarily, increasing build times.
## 7. Build Tooling and Integrations
### 7.1. Gradle Enterprise
**Do This:** Consider using Gradle Enterprise for advanced build caching, build scans, and performance analytics.
**Don't Do This:** Neglect to use advanced tooling that can provide insights into build performance and identify optimization opportunities.
**Why:** Gradle Enterprise provides detailed build scans and performance analytics, helping you identify and address performance bottlenecks.
**Example:** Integrate Gradle Enterprise into your build environment.
**Anti-Pattern:** Not leveraging advanced tooling for build performance analysis.
### 7.2. CI/CD Integration
**Do This:** Integrate Gradle with your CI/CD system to automate builds and tests.
**Don't Do This:** Perform manual builds and tests, which can be time-consuming and error-prone.
**Why:** CI/CD integration automates the build and test process, improving developer productivity and ensuring consistent builds.
**Example:** Configure your CI/CD system to run Gradle builds and tests.
**Anti-Pattern:** Performing manual builds and tests, which is inefficient and error-prone.
By adhering to these performance optimization standards, Gradle builds can achieve their full potential. These guidelines should serve as a reference point for all developers working with Gradle projects.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# State Management Standards for Gradle This document outlines the coding standards for managing state within Gradle builds and plugins. Effective state management is crucial for creating reliable, reproducible, and maintainable builds. It specifically addresses how Gradle projects and plugins should handle data that persists across tasks or configurations. ## 1. Principles of State Management in Gradle Managing state in Gradle is significantly different from managing state in a typical application. Gradle's build system is designed to be declarative and (ideally) idempotent. Therefore, state should be managed with these goals in mind. * **Immutability:** Favor immutable data structures. This prevents accidental modifications and simplifies reasoning about the build process. * **Reproducibility:** Strive to make builds reproducible. This means that, given the same input state, the build should produce the same output. Avoid relying on external state that can change unexpectedly. * **Explicit Dependencies:** Declare dependencies explicitly. Gradle's dependency management system is powerful; use it to your advantage. * **Avoid Global State:** Minimize the use of global state in your build scripts and plugins. Global state can lead to unexpected side effects and make builds difficult to reason about. * **Task Inputs and Outputs:** Use task inputs and outputs to track and manage the state of individual tasks. Gradle uses this information to determine whether a task needs to be executed. * **Configuration Cache Compatibility:** Design your build scripts and plugins to be compatible with Gradle's Configuration Cache feature. This requires careful consideration of state management. * **Incremental Build Support:** Design your tasks to support incremental builds by properly defining inputs and outputs. ## 2. Configuration Phase State Management The configuration phase in Gradle involves evaluating the "build.gradle.kts" script and associated plugins. State managed during this phase impacts how the build is configured. ### 2.1. Project Properties and Extensions Project properties and extra properties are mechanisms for storing state that can be accessed throughout the build script. Extensions provide a structured way to encapsulate related properties. **Do This:** * Use project extensions to group related properties, providing a clear structure for your build configuration. * Define extensions within plugins to encapsulate build logic and configuration. * Use "providers" for properties where the value may not be known at configuration time. Providers allow for lazy evaluation. * Prefer Kotlin DSL's typed accessors for safer and more readable property access. **Don't Do This:** * Don't pollute the global namespace with too many project-level properties. This can lead to naming conflicts and make it difficult to understand the build configuration. * Don't store complex objects directly in project properties. Use extensions to encapsulate object logic. * Don't eagerly calculate property values if they aren't immediately needed. **Example:** """kotlin // build.gradle.kts plugins { id("org.example.my-plugin") } myPlugin { apiEndpoint.set("https://api.example.com") timeoutSeconds.set(30) } tasks.register("printConfig") { doLast { println("API Endpoint: ${myPlugin.get().apiEndpoint.get()}") println("Timeout: ${myPlugin.get().timeoutSeconds.get()}") } } // src/main/kotlin/org/example/MyPlugin.kt package org.example import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.provider.Property import org.gradle.kotlin.dsl.* interface MyPluginExtension { val apiEndpoint: Property<String> val timeoutSeconds: Property<Int> } class MyPlugin : Plugin<Project> { override fun apply(project: Project) { val extension = project.extensions.create<MyPluginExtension>("myPlugin") { apiEndpoint.convention("https://default.example.com") timeoutSeconds.convention(60) } } } """ **Explanation:** * The "MyPluginExtension" interface defines the properties for the plugin. * The "apiEndpoint" and "timeoutSeconds" properties are defined as "Property<String>" and "Property<Int>" respectively, allowing users to configure them in the "build.gradle.kts" file. * The "convention" method sets default values for the properties. * Kotlin DSL accessors are used. The generated accessor for the created extension allows the plugin user to access the "myPlugin" extension in a type-safe way. * Lazy evaluation is used through ".get()", so values are only fetched when needed. ### 2.2. Configuration Cache Considerations The Configuration Cache is a performance optimization that caches the result of the configuration phase. To support it, you must ensure that your configuration logic is serializable and doesn't rely on external state that can change between builds. **Do This:** * Ensure that all objects stored in project properties or extensions are serializable. Prefer simple data types like String, Int, and Boolean. If using complex objects, implement "java.io.Serializable" or, even better, use Gradle's "BuildService" API with injected state. * Use "providers" to defer the reading of external state (e.g., environment variables) until task execution. * Avoid using non-serializable classes or objects within your plugins. * Annotate non-serializable fields with "@Transient" if absolutely necessary. However, try to avoid this. **Don't Do This:** * Don't store non-serializable objects in project properties or extensions without careful consideration. * Don't read environment variables or system properties directly during the configuration phase. * Don't use singleton objects with mutable state within your build configuration logic. **Example:** """kotlin // build.gradle.kts plugins { id("org.example.config-cache-plugin") } the<ConfigCacheExtension>().apply { message.set("Hello, Configuration Cache!") } tasks.register("configCacheTask") { doLast { println(the<ConfigCacheExtension>().message.get()) } } // src/main/kotlin/org/example/ConfigCachePlugin.kt package org.example import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.provider.Property import org.gradle.kotlin.dsl.create import org.gradle.kotlin.dsl.the interface ConfigCacheExtension { val message: Property<String> } class ConfigCachePlugin : Plugin<Project> { override fun apply(project: Project) { project.extensions.create<ConfigCacheExtension>("configCacheExtension") { message.convention("Default message") } } } """ **Explanation:** * The "ConfigCacheExtension" interface defines a single "message" property of type "Property<String>". * The "message" property is configured with a default value using the "convention" method. * A task "configCacheTask" is registered to print the value of the "message" property at execution time. Crucially, the property value is not accessed during the configuration phase. ## 3. Task Phase State Management Tasks are the fundamental units of execution in Gradle. Managing task state involves defining inputs, outputs, and actions that modify the build environment. ### 3.1. Task Inputs and Outputs Task inputs define the data that a task consumes, while task outputs define the data that a task produces. Gradle uses this information to determine whether a task needs to be executed. **Do This:** * Declare all task inputs and outputs explicitly using the "@Input", "@OutputDirectory", "@OutputFile", "@InputFile", "@InputFiles", "@InputDirectory", "@Classpath", "@Optional", and "@Console" annotations (or their equivalent DSL methods). * Use "incrementalTaskInput" for tasks that can process only changed input files. * Use property annotations like "@Input", "@Optional", and "@InputDirectory" along with abstract classes and "abstract val" to define task inputs in a type-safe way. * Use Gradle's built-in file system operations for managing task outputs to ensure consistency and correctness. * Use "TaskProvider" when declaring task dependencies to allow for lazy configuration. * Use the "up-to-date checks" functionality to avoid unnecessary task executions. * Use "@Internal" for properties that represent internal state of the task and should not be considered for up-to-date checks. **Don't Do This:** * Don't assume that a task will always be executed. Gradle may skip task execution if the inputs and outputs haven't changed. * Don't rely on implicit task dependencies. Declare dependencies explicitly using "dependsOn". * Don't modify files outside of the declared task outputs. This can lead to unexpected side effects and make builds unreliable. **Example:** """kotlin import org.gradle.api.DefaultTask import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.RegularFileProperty import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Property import org.gradle.api.tasks.* import javax.inject.Inject abstract class MyTask @Inject constructor(objects: ObjectFactory) : DefaultTask() { @get:Input abstract val message: Property<String> @get:InputFile @get:PathSensitive(PathSensitivity.RELATIVE) abstract val inputFile: RegularFileProperty @get:OutputDirectory abstract val outputDir: DirectoryProperty @TaskAction fun run() { val inputFilePath = inputFile.get().asFile.absolutePath val outputDirPath = outputDir.get().asFile.absolutePath val messageValue = message.get() // Simulate processing the input file and writing to the output directory val outputFile = outputDir.get().file("output.txt").asFile outputFile.writeText("Message: $messageValue\nInput File: $inputFilePath") println("Task executed: $name") println("Message: $messageValue") println("Input File: $inputFilePath") println("Output File: $outputFile") } } // build.gradle.kts tasks.register<MyTask>("myTask") { message.set("Hello from MyTask!") inputFile.set(file("input.txt")) outputDir.set(file("output")) } """ **Explanation:** * The "MyTask" class extends "DefaultTask" and defines several input and output properties. * The "@Input" annotation declares the "message" property as an input to the task. * The "@InputFile" and "@OutputDirectory" annotations declare the "inputFile" and "outputDir" properties as file inputs and directory outputs, respectively. * The "@PathSensitive(PathSensitivity.RELATIVE)" annotation specifies that the task should be considered out-of-date if the relative path of the input file changes. * The "run" method performs the task's action, reading the input file, processing it, and writing the output to the output directory. This demonstrates how to access the values of inputs and outputs defined as properties. * The "objects: ObjectFactory" is required for property injection into the abstract class. * The build script registers an instance of "MyTask" using "tasks.register<MyTask>("myTask")" ### 3.2. Incremental Tasks Incremental tasks are tasks that can process only the changed input files, rather than reprocessing all input files. This can significantly improve build performance. **Do This:** * Use the "incrementalTaskInput" method to define incremental task inputs. * Use "InputChanges" to determine which inputs have changed since the last task execution. * Use appropriate mechanisms (e.g., file hashing, timestamps) to track changes to input files. **Don't Do This:** * Don't reprocess all input files if only a subset of files has changed. * Don't rely on external state to determine which files have changed. **Example:** """kotlin import org.gradle.api.DefaultTask import org.gradle.api.tasks.* import org.gradle.api.file.FileCollection import org.gradle.api.provider.Property import javax.inject.Inject import org.gradle.api.model.ObjectFactory import org.gradle.api.tasks.incremental.IncrementalTaskInputs abstract class IncrementalCopyTask @Inject constructor(objects: ObjectFactory) : DefaultTask() { @get:InputDirectory abstract val sourceDir: Property<File> @get:OutputDirectory abstract val targetDir: Property<File> @TaskAction fun copyFiles(inputChanges: IncrementalTaskInputs) { if (!inputChanges.isIncremental) { println("Performing full copy as task input is not incremental") targetDir.get().asFile.deleteRecursively() } inputChanges.outOfDate { change -> val sourceFile = change.file val targetFile = targetDir.get().file(change.path).asFile println("Copying ${sourceFile.name} to ${targetFile.path}") sourceFile.copyTo(targetFile, overwrite = true) } inputChanges.removed { change -> val targetFile = targetDir.get().file(change.path).asFile println("Deleting ${targetFile.path}") targetFile.delete() } } } // build.gradle.kts tasks.register<IncrementalCopyTask>("incrementalCopy") { sourceDir.set(file("src/main/resources")) targetDir.set(file("build/resources/main")) } """ **Explanation:** * This task incrementally copies files from a source directory to a target directory. * "IncrementalTaskInputs" is used to determine which files have been added, removed, or modified since the last execution * "inputChanges.isIncremental" allows the task to do optimized processing when running incrementally. * The injected "ObjectFactory" is used to create the "Property" instances. ### 3.3. Build Services Build services are a mechanism for sharing state between tasks and across builds. They are particularly useful for managing resources that are expensive to create or for sharing data between tasks that are not directly related. They are also configuration cache compatible. **Do This:** * Use build services to manage shared resources, such as database connections or API clients. * Define build services as abstract classes or interfaces and register them using "gradle.sharedServices". * Use the "@Inject" annotation to inject build services into tasks. * Consider using "AutoCloseable" to manage the lifecycle of resources held by build services. **Don't Do This:** * Don't use global variables or static fields to share state between tasks. * Don't create build services that are not properly configured or initialized. * Don't forget to release resources held by build services when they are no longer needed. **Example:** """kotlin import org.gradle.api.provider.Property import org.gradle.api.services.BuildService import org.gradle.api.services.BuildServiceParameters import org.gradle.api.tasks.TaskAction import org.gradle.api.DefaultTask import org.gradle.api.tasks.Input import org.gradle.api.Project import org.gradle.api.provider.Provider import org.gradle.kotlin.dsl.* import javax.inject.Inject import org.gradle.api.model.ObjectFactory interface MyBuildServiceParameters : BuildServiceParameters { val message: Property<String> } abstract class MyBuildService @Inject constructor(objects: ObjectFactory): BuildService<MyBuildServiceParameters>, AutoCloseable { private val id = System.identityHashCode(this) init { parameters.message.convention("Default message from service $id") println("Build service $id created with message: ${parameters.message.get()}") } fun doSomething(): String { return "Build service $id says: ${parameters.message.get()}" } override fun close() { println("Build service $id is closing") } } abstract class MyTask @Inject constructor(objects : ObjectFactory): DefaultTask() { @get:Input abstract val taskMessage: Property<String> @TaskAction fun run() { val serviceProvider: Provider<MyBuildService> = project.gradle.sharedServices.registerIfAbsent("my-service", MyBuildService::class) { parameters { message.set(taskMessage) } } val service = serviceProvider.get() println("Task '${name}' executed. " + service.doSomething()) } } // build.gradle.kts gradle.sharedServices.registerIfAbsent("my-shared-service", MyBuildService::class) { parameters { message.set("Message from build.gradle.kts") } } tasks.register<MyTask>("myTask") { taskMessage.set("Hello from MyTask config!") } """ **Explanation:** * The "MyBuildService" class implements the "BuildService" interface and defines a single "message" parameter. * The "MyTask" class uses the "gradle.sharedServices" method to register the "MyBuildService" and obtain a provider for it. * The "run" method obtains an instance of the "MyBuildService" from the provider and uses it to perform some work. * The "AutoCloseable" interface is implemented to release resources when the build service is no longer needed. Gradle will automatically call the "close()" method when the build service is no longer in use, even if the build fails. ### 3.4 Task Dependencies and Ordering Task dependencies define the order in which tasks are executed. Proper management of task dependencies is essential for ensuring that tasks are executed in the correct order and that the build process is efficient. **Do This:** * Declare task dependencies explicitly using the "dependsOn" method. * Use "mustRunAfter" and "shouldRunAfter" to specify ordering constraints between tasks. * Use "TaskProvider"s when specifying task dependencies to allow for lazy configuration. * Leverage Gradle's task ordering features to optimize build performance and ensure correctness. **Don't Do This:** * Don't rely on implicit task dependencies. * Don't create circular task dependencies. * Don't create overly complex task dependency graphs. **Example:** """kotlin // build.gradle.kts val taskA = tasks.register("taskA") { doLast { println("Executing taskA") } } val taskB = tasks.register("taskB") { dependsOn(taskA) doLast { println("Executing taskB") } } tasks.register("taskC") { mustRunAfter(taskB) doLast { println("Executing taskC") } } tasks.register("taskD") { shouldRunAfter(taskC) doLast { println("Executing taskD") } } """ **Explanation:** * "taskB" depends on "taskA", meaning that "taskA" will be executed before "taskB". * "taskC" *must* run after "taskB". * "taskD" *should* run after "taskC". This is a weaker constraint than "mustRunAfter" and allows Gradle to potentially execute "taskC" and "taskD" in parallel if possible. ## 4. Security Considerations When managing state in Gradle, it's important to consider security implications. Build scripts and plugins can access sensitive information, such as API keys, passwords, and certificates. You must protect this information from unauthorized access. **Do This:** * Use environment variables or Gradle properties to store sensitive information. * Avoid hardcoding sensitive information in build scripts or plugins. * Use build scans to track the state of your builds and identify potential security vulnerabilities. * Use secure communication protocols (e.g., HTTPS) when accessing external resources. * Follow secure coding practices when developing Gradle plugins. **Don't Do This:** * Don't store sensitive information in version control. * Don't expose sensitive information in error messages or logs. * Don't use insecure communication protocols (e.g., HTTP) when accessing external resources. * Don't trust external code without proper validation. **Example:** """kotlin // build.gradle.kts tasks.register("secureTask") { val apiKey = providers.environmentVariable("API_KEY").orElse(providers.gradleProperty("apiKey")).get() doLast { println("Using API key: $apiKey") // In real code, don't print the key! Use it securely. } } """ **Explanation:** * The "secureTask" task retrieves the API key from an environment variable or a Gradle property. * The "environmentVariable" and "gradleProperty" methods provide a secure way to access sensitive information. "orElse" specifies a fallback if the environment variable is not set. * **Important:** In real code, you should *never* print the API key to the console. This is just an example to show how to retrieve it. Instead, use the API key securely within the task. ## 5. Modern Approaches and Patterns Gradle development continuosly evolves. Here are some modern best-practices that should be followed: * Using dependency injection and Providers for lazy evaluation of properties. * Creating custom Gradle managed entities to store global state across the build. * Leveraging Build Scans to view global state of the Gradle build. ## 6. Deprecated Features and Known Issues * The "ext" block in Gradle is considered legacy. While it still works, prefer "extensions" for better structure and type safety, particularly with Kotlin DSL. * The Settings API in Gradle is under active development. Breaking changes can sometimes occur between minor releases. Refer to the release notes for each Gradle version to stay informed. * Avoid using the "allprojects" and "subprojects" blocks in favor of more explicit configuration of the projects to apply a certain configuration.
# Code Style and Conventions Standards for Gradle This document outlines the code style and conventions standards for Gradle projects. Adhering to these guidelines will improve code readability, maintainability, and consistency across projects. This document aims to provide a definitive guide to Gradle development best practices applicable to professional development teams and to serve as context for AI coding assistants. ## 1. General Principles * **Consistency:** Adhere to these standards consistently throughout the codebase. * **Readability:** Prioritize code that is easy to read and understand. * **Maintainability:** Write code that is easy to modify and extend. * **Clarity:** Code should be self-documenting as much as possible. Use comments strategically to explain complex logic or non-obvious intent. * **Simplicity:** Strive for the simplest solution that meets the requirements. Avoid over-engineering. ## 2. Formatting ### 2.1. Indentation * **Do This:** Use 4 spaces for indentation. Avoid using tabs. * **Why:** Consistent indentation improves readability by visually structuring the code. * **Tooling:** Configure your IDE to automatically format code according to these preferences. Consider using EditorConfig (.editorconfig) to enforce these settings across different IDEs. """properties # .editorconfig root = true [*] indent_style = space indent_size = 4 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true """ * **Don't Do This:** Mix spaces and tabs. Use inconsistent indentation. """gradle // Anti-pattern: Inconsistent indentation task myTask { if (true) { println "Hello" } } """ ### 2.2. Line Length * **Do This:** Limit lines to a maximum of 120 characters. * **Why:** Long lines make code harder to read, especially on smaller screens. * **Formatting:** Break long lines into multiple shorter lines, using appropriate indentation to maintain readability. * **Don't Do This:** Allow lines to exceed 120 characters. """gradle // Anti-pattern: Long line dependencies { implementation "com.example:very-long-artifact-name:1.0.0" // This line is too long! } """ """gradle // Do This: Break long lines dependencies { implementation( "com.example:very-long-artifact-name:1.0.0" ) } """ ### 2.3. Blank Lines * **Do This:** Use blank lines to separate logical sections of code (e.g., between methods, between properties, within tasks). * **Why:** Blank lines improve readability by grouping related code together. * **Don't Do This:** Use excessive blank lines or no blank lines at all. """gradle // Good example plugins { id 'java' } group = 'com.example' version = '1.0-SNAPSHOT' repositories { mavenCentral() } dependencies { implementation 'org.apache.commons:commons-lang3:3.12.0' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0' } test { useJUnitPlatform() } """ ### 2.4. Braces * **Do This:** Use braces for all control flow statements (if, else, for, while), even if the block contains only one statement. * **Why:** Braces improve readability and prevent errors when modifying the code later. * **Don't Do This:** Omit braces for single-statement blocks. """gradle // Anti-pattern: Omitting braces if (true) println "Hello" // Do This: Always use braces if (true) { println "Hello" } """ ### 2.5. Spacing * **Do This:** Use spaces around operators (=, +, -, *, /, etc.), after commas, and after keywords (if, for, while). * **Why:** Proper spacing improves readability. * **Don't Do This:** Omit spaces around operators or after commas. """gradle // Anti-pattern: Poor spacing def x=1+2;if(x>0){println"Positive"} // Do This: Proper spacing def x = 1 + 2 if (x > 0) { println "Positive" } """ ## 3. Naming Conventions ### 3.1. Tasks * **Do This:** Use camelCase for task names. Use descriptive names that clearly indicate the task's purpose. * **Why:** Clear and consistent task naming makes it easier to understand and manage the build process. * **Examples:** "compileJava", "test", "assemble", "generateDocs", "createRelease". """gradle task generateSources { doLast { println "Generating sources..." } } """ * **Don't Do This:** Use overly short or ambiguous task names. Use names that conflict with existing Gradle tasks. """gradle // Anti-pattern: Vague name task x { doLast { println "Doing something..." } } """ ### 3.2. Properties * **Do This:** Use camelCase for property names. Use descriptive names that clearly indicate the property's purpose. * **Why:** Consistent property naming makes code easier to understand and maintain. * **Examples:** "sourceCompatibility", "targetCompatibility", "outputDirectory". """gradle ext { myCustomProperty = "someValue" } """ * **Don't Do This:** Use overly short or ambiguous property names. Use names that conflict with existing Gradle properties. """gradle // Anti-pattern: Vague property name ext { x = "someValue" } """ ### 3.3. Variables * **Do This:** Use camelCase for variable names. Follow the same principles as properties. * **Why:** Consistency is key to readability. * **Don't Do This:** Single-letter variable names (except for loop counters like "i" or "j" in very localized contexts). """gradle // Anti-pattern: Unclear variable name task myTask { doLast { def a = "Hello" println a } } // Do This: Descriptive variable name task myTask { doLast { def greetingMessage = "Hello" println greetingMessage } } """ ## 4. Style Preferences ### 4.1. Explicit Typing When Necessary * **Do This:** Use explicit typing (e.g., "String", "Integer", "List") when it improves readability and maintainability, especially for complex types or when the type is not immediately obvious from the context. However, Gradle and Groovy promote convention over configuration, so avoid being overly verbose. * **Why:** Explicit typing can help prevent type-related errors and make the code easier to understand, especially for newcomers. * **Don't Do This:** Avoid repeating the type when it's already clear in the assignment, leverage "def" in these scenarios. """gradle // Good example: Explicit type task myTask { doLast { String message = "Hello, explicit typing!" println message } } // Good Example: Implicit (def) Typing task myTask2 { doLast { def message = "Hello implicit typing - cleaner!" println message } } """ ### 4.2. String Interpolation * **Do This:** Prefer string interpolation using ""${variable}"" over string concatenation using "+". * **Why:** String interpolation is more readable and concise. * **Examples:** """gradle def name = "Gradle" println "Hello, ${name}!" // Preferred println "Hello, " + name + "!" // Less readable """ * **Don't Do This:** Overuse string concatenation, specifically when string interpolation presents a cleaner alternative. ### 4.3. Closures * **Do This:** Use trailing closures when appropriate for readability. Consider using implicit "it" parameter within closures when the intent is crystal clear. * **Why:** Trailing closures make code more concise and readable. The "it" parameter is Groovy's default closure argument so omitting it helps to reduce visual clutter. * **Examples:** """gradle // Sample Data def myList = [ "item1", "item2", "item3"] //Trailling Closures myList.each { item -> println item } myList.each { println it //Implicit "it" parameter } """ * **Anti-pattern:** Ignoring trailing closure syntax opportunities or being overly verbose within closures. ### 4.4. Using the "settings.gradle(.kts)" File * **Do This:** Keep the "settings.gradle(.kts)" file focused on project inclusion and configuration. Avoid putting build logic in this file. Use the init script for global configuration. * **Why:** Separation of concerns improves maintainability and understandability. The settings file focuses on project structure, while the build files focus on the project's build logic. * **Don't Do This:** Place complex build logic in "settings.gradle(.kts)". """gradle // settings.gradle.kts rootProject.name = 'my-project' include('module1', 'module2') """ ### 4.5. Version Catalogs (Recommended) * **Do This:** Use version catalogs to manage dependencies in a centralized and type-safe manner (starting with Gradle 7.0). * **Why**: Centralized dependency management promotes consistency and reduces duplication. Also provides type-safe access to dependencies. * **Example:** 1. Create "gradle/libs.versions.toml": """toml [versions] guava = "32.1.2-jre" junit = "5.10.0" [libraries] guava = { module = "com.google.guava:guava", version.ref = "guava" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } [bundles] test = ["junit-jupiter-api", "junit-jupiter-engine"] [plugins] # If appropriate """ 2. Use the catalog in "build.gradle.kts": """kotlin dependencies { implementation(libs.guava) testImplementation(libs.bundles.test) } """ * **Don't Do This:** Hardcode dependency versions in multiple build files. ### 4.6. Lazy Evaluation and Configuration Avoidance * **Do This:** Utilize Gradle's lazy evaluation and configuration avoidance features (e.g., "registering" tasks, "map { }", "flatMap { }") to improve build performance. * **Why:** Lazy evaluation and configuration avoidance prevent unnecessary work from being done during the configuration phase, resulting in faster build times. * **Example:** """gradle // Instead of task(type: Copy, 'copyDocs') { from 'src/docs' into 'build/docs' } //Use registering (configuration avoidance) tasks.register('copyDocs', Copy) { from 'src/docs' into 'build/docs' } """ * **Don't Do This:** Eagerly configure tasks or dependencies that are not always needed. ### 4.7. Convention Plugins * **Do This:** Create convention plugins to encapsulate reusable build logic and configurations. Apply these plugins to multiple projects. * **Why:** Convention plugins promote code reuse, reduce duplication, and improve maintainability of large projects. * **Example:** 1. Create a file "buildSrc/src/main/kotlin/my-java-conventions.gradle.kts": """kotlin plugins { java id("org.springframework.boot") version ("3.2.0") apply false //Example of external plugin } dependencies { implementation("org.apache.commons:commons-lang3:3.12.0") } java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } """ 2. Apply the plugin in "build.gradle.kts": """kotlin plugins { id("my-java-conventions") } """ * **Don't Do This:** Duplicate build logic across multiple build files. ### 4.8. Plugin Management using "plugins {}" block * **Do This:** Prefer the "plugins {}" block to apply plugins instead of the older "apply plugin:" syntax in "build.gradle.kts". * **Why:** The "plugins {}" block is type-safe, more readable, and allows Gradle to perform better dependency resolution/plugin management. * **Example:** """kotlin plugins { kotlin("jvm") version "1.9.21" } """ * **Don't Do This:** Use the "apply plugin:" syntax except in legacy scenarios. """gradle //Anti-pattern apply plugin: 'kotlin' //Avoid this approach """ ### 4.9 Using Gradle Properties * **Do This:** Use the "gradle.properties" file or command-line arguments ("-P") for externalizing configuration parameters that may vary in different environments. * **Why:** This approach allows for a flexible and standardized way to configure builds for different scenarios, such as CI/CD pipelines or local development. * **Example:** 1. In "gradle.properties": """properties version=1.2.3 some.api.key=abcdef123456 """ 2. Then reference in "build.gradle.kts": """kotlin version = findProperty("version") ?: "1.0.0-SNAPSHOT" //Default value if not defined val apiKey = findProperty("some.api.key") """ * **Don't Do This:** Hardcode environment-specific configuration directly in the build scripts or ignore setting default values. ### 4.10 Secure Coding Practices * **Do This:** Avoid hardcoding sensitive information (e.g., passwords, API keys) in build scripts. Use environment variables, Gradle properties (encrypted), or dedicated secret management solutions. * **Why:** Protect sensitive information from being exposed in source control or build artifacts. * **Don't Do This:** Commit sensitive information to source control or expose it in build logs. Consider using tools like "git-secrets" to avoid accidental commits. ## 5. Documentation * **Do This:** Provide clear and concise documentation for custom tasks, plugins, and configurations. Explain the purpose, usage, and any relevant dependencies. Use KDoc style comments. * **Why:** Documentation makes it easier for others (and yourself in the future) to understand and maintain the codebase. * **Don't Do This:** Omit documentation or provide incomplete or outdated documentation. By adhering to these coding style and conventions, you can create Gradle builds that are maintainable, readable, and performant. This document serves as a starting point, and you may need to adapt these guidelines for specific projects or teams. Regularly review and update these standards to reflect the latest best practices and changes in the Gradle ecosystem.
# Core Architecture Standards for Gradle This document outlines the core architecture standards for Gradle projects, designed to promote maintainability, performance, and security. It serves as a guide for developers and provides context for AI coding assistants. These standards reflect modern best practices and leverage the latest Gradle features. ## 1. Project Structure and Organization A well-defined project structure promotes discoverability, reduces complexity, and simplifies maintenance. ### 1.1 Multi-Project Builds For non-trivial projects, adopt a multi-project build structure. This modular approach improves build times, encourages code reuse, and isolates concerns. * **Do This:** Organize your code into logical modules based on functionality or domain. * **Don't Do This:** Lump all code into a single, monolithic project. * **Why This Matters:** Improves build performance (parallel execution), enhances code reusability, and simplifies refactoring. """gradle // settings.gradle.kts rootProject.name = "my-application" include("core") include("api") include("service") include("client") """ """gradle // core/build.gradle.kts plugins { java } dependencies { implementation("org.apache.commons:commons-lang3:3.12.0") } """ * **Anti-Pattern:** A single large "build.gradle.kts" file containing all dependencies and configurations. ### 1.2 Standard Directory Layout Follow the standard Gradle directory layout for each project. * **Do This:** Use "src/main/java" for production Java code, "src/test/java" for test code, "src/main/resources" for resources, and "src/test/resources" for test resources. * **Don't Do This:** Deviate from the standard layout without a compelling reason. Custom source sets should be used sparingly and documented clearly. * **Why This Matters:** Enables Gradle to automatically recognize and process source files, reducing configuration overhead. * **Example:** """ my-application/ ├── settings.gradle.kts ├── core/ │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/example/core/ │ │ └── CoreClass.java │ └── test/ │ └── java/ │ └── com/example/core/ │ └── CoreClassTest.java ├── api/ │ └── ... └── service/ └── ... """ ### 1.3 Convention over Configuration Embrace Gradle's convention-over-configuration approach. Leverage sensible defaults provided by plugins. * **Do This:** Use plugins and let them handle common tasks such as compilation and testing. * **Don't Do This:** Manually configure tasks that are already handled by plugins, unless customization is absolutely necessary. * **Why This Matters:** Reduces build script complexity and makes the build more maintainable. """gradle // build.gradle.kts plugins { java } repositories { mavenCentral() } dependencies { implementation("org.springframework:spring-core:6.1.0") testImplementation("org.junit.jupiter:junit-jupiter-api:5.11.0-M1") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.11.0-M1") } tasks.test { useJUnitPlatform() } """ ## 2. Modularization and Abstraction Proper modularization and abstraction are critical for large projects. Aim for loosely coupled, highly cohesive modules. ### 2.1 Domain-Driven Design (DDD) Consider applying DDD principles to structure your modules around domain concepts. * **Do This:** Create modules that represent bounded contexts or aggregates from your domain. * **Don't Do This:** Create modules that are purely technical or infrastructural. * **Why This Matters:** Improves code understandability, promotes easier changes, and aligns the codebase with the business domain. DDD practices should be well-documented for clarity. ### 2.2 Interface-Based Design Prefer interface-based design to decouple modules. * **Do This:** Define interfaces in one module and implement them in another. * **Don't Do This:** Directly depend on concrete classes in other modules. * **Why This Matters:** Reduces dependencies and increases flexibility. """java // api/src/main/java/com/example/api/GreetingService.java package com.example.api; public interface GreetingService { String greet(String name); } """ """java // service/src/main/java/com/example/service/GreetingServiceImpl.java package com.example.service; import com.example.api.GreetingService; public class GreetingServiceImpl implements GreetingService { @Override public String greet(String name) { return "Hello, " + name + "!"; } } """ """gradle // service/build.gradle.kts dependencies { implementation(project(":api")) } """ ### 2.3 Dependency Injection (DI) Use DI to manage dependencies between modules. frameworks like Spring or Guice can be used if complexity warrants. Otherwise, constructor injection works well with modern Kotlin. * **Do This:** Inject dependencies into classes instead of creating them directly. * **Don't Do This:** Use service locators or singleton patterns excessively for dependency management. * **Why This Matters:** Reduces coupling, improves testability, and simplifies configuration. """java // service/src/main/java/com/example/service/MyService.java package com.example.service; import com.example.api.GreetingService; public class MyService { private final GreetingService greetingService; public MyService(GreetingService greetingService) { this.greetingService = greetingService; } public String doSomething(String name) { return greetingService.greet(name); } } """ ## 3. Build Script Design Clean and well-structured build scripts are crucial for maintainability. ### 3.1 Kotlin DSL Use the Kotlin DSL for Gradle build scripts. This offers superior type safety, IDE support, and readability compared to Groovy. * **Do This:** Write all new build scripts using Kotlin DSL ("*.gradle.kts"). * **Don't Do This:** Use Groovy DSL ("*.gradle") for new projects. Migrate existing Groovy builds when feasible. * **Why This Matters:** Improves maintainability, reduces errors, and provides better IDE integration (autocompletion, refactoring). """kotlin // build.gradle.kts plugins { kotlin("jvm") version "1.9.21" } group = "com.example" version = "1.0-SNAPSHOT" repositories { mavenCentral() } dependencies { implementation(kotlin("stdlib")) testImplementation("org.junit.jupiter:junit-jupiter-api:5.11.0-M1") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.11.0-M1") } tasks.test { useJUnitPlatform() } """ ### 3.2 Build Logic Extraction Extract common build logic into reusable functions or plugins. * **Do This:** Identify repetitive tasks or configurations and move them into separate files ("buildSrc" directory, custom plugins, or script plugins). * **Don't Do This:** Duplicate build logic across multiple projects. * **Why This Matters:** Reduces redundancy, simplifies maintenance, and ensures consistency. """kotlin // buildSrc/src/main/kotlin/my-conventions.gradle.kts plugins { id("java") } dependencies { implementation("org.apache.commons:commons-lang3:3.12.0") } tasks.test { useJUnitPlatform() } """ """gradle // build.gradle.kts plugins { id("my-conventions") } dependencies { implementation("org.springframework:spring-core:6.1.0") // extra dependency } """ ### 3.3 Version Catalogues Use version catalogues to manage dependencies and plugins versions centrally. This ensures consistency across the build. * **Do This:** Define all dependency and plugin versions in the "gradle/libs.versions.toml" file. * **Don't Do This:** Hardcode versions directly in the "build.gradle.kts" files. * **Why This Matters:** Simplifies dependency management, ensures consistency, and reduces the risk of conflicts. """toml # gradle/libs.versions.toml [versions] springBoot = "3.2.0" kotlin = "1.9.21" junit = "5.11.0-M1" [libraries] spring-web = { module = "org.springframework.boot:spring-web", version.ref = "springBoot" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } spring-boot = { id = "org.springframework.boot", version.ref = "springBoot" } spring-dependency-management = { id = "io.spring.dependency-management", version = "1.1.4" } """ """gradle // build.gradle.kts plugins { alias(libs.plugins.kotlin.jvm) alias(libs.plugins.spring.boot) alias(libs.plugins.spring.dependency.management) } dependencies { implementation(libs.spring.web) implementation(libs.kotlin.stdlib) testImplementation(libs.junit.jupiter.api) testRuntimeOnly(libs.junit.jupiter.engine) } """ ### 3.4 Task Configuration Avoidance Use configuration avoidance APIs when configuring tasks, especially for large projects. These APIs delay task configuration until absolutely necessary, decreasing configuration time. * **Do This:** Use "tasks.register" instead of "tasks.create" or direct property assignment where possible. Use "named" instead of direct access to task properties when configuring. * **Don't Do This:** Eagerly configure tasks that might not be executed. * **Why This Matters:** Decreases build configuration time, especially in large multi-project builds. """kotlin // build.gradle.kts tasks.register<Copy>("copyDocs") { from("src/docs") into("build/docs") } tasks.named<Test>("test") { useJUnitPlatform() } """ ### 3.5 Incremental Builds Leverage Gradle's incremental build capabilities to avoid unnecessary work. * **Do This:** Ensure that your tasks are properly configured for incremental builds (e.g., declare inputs and outputs). Use "@Input", "@OutputDirectory", "@InputFile", "@InputChanges", etc., annotations. * **Don't Do This:** Write tasks that always execute from scratch, even when inputs haven't changed. The "@TaskAction" method should be as lean as possible. * **Why This Matters:** Significantly reduces build times by only re-executing tasks when necessary. """java // src/main/java/com/example/customtask/GenerateFileTask.java package com.example.customtask; import org.gradle.api.DefaultTask; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.RegularFileProperty; import org.gradle.api.provider.Property; import org.gradle.api.tasks.*; import java.io.File; import java.io.FileWriter; import java.io.IOException; public abstract class GenerateFileTask extends DefaultTask { @Input public abstract Property<String> getMessage(); @OutputDirectory public abstract DirectoryProperty getOutputDir(); @TaskAction public void generate() throws IOException { File outputDir = getOutputDir().get().getAsFile(); if (!outputDir.exists()) { outputDir.mkdirs(); } File outputFile = new File(outputDir, "message.txt"); try (FileWriter writer = new FileWriter(outputFile)) { writer.write(getMessage().get()); } } } """ """kotlin // build.gradle.kts tasks.register<com.example.customtask.GenerateFileTask>("generateMessage") { message.set("Hello, Gradle!") outputDir.set(file("build/generated")) } """ ## 4. Dependency Management Efficient dependency management is crucial for build stability and performance. ### 4.1 Consistent Dependency Versions Enforce consistent versions for all dependencies. Transitive dependency management can lead to version conflicts. Use dependency constraints or BOMs (Bill of Materials) to manage dependency versions centrally. Version catalogues are also useful in this regard. * **Do This:** Define dependency versions consistently across all modules. Use "dependencyConstraints" or BOMs to enforce version consistency. Version catalogues (as described above) are an excellent option. * **Don't Do This:** Allow inconsistent dependency versions across the project. * **Why This Matters:** Prevents runtime errors and ensures that all modules use compatible versions of dependencies. Resolves dependency conflicts. """gradle // build.gradle.kts dependencies { implementation("org.springframework:spring-core:6.1.0") implementation("org.springframework:spring-context:6.1.0") } dependencyConstraints { implementation("org.springframework:spring-core:6.1.0") { because("Ensures consistent Spring Core version") } } """ ### 4.2 Dynamic Versions Avoidance Avoid using dynamic versions (e.g., "1.0.+", "latest.release") for dependencies. * **Do This:** Specify explicit, fixed versions for all dependencies. * **Don't Do This:** Use dynamic versions, as they can lead to unpredictable builds. * **Why This Matters:** Ensures that the build is reproducible and that dependencies are consistent across builds. Prevents unexpected behavior due to dependency updates. ### 4.3 Repository Management Configure repositories correctly and securely. * **Do This:** Declare only the necessary repositories. Use HTTPS for all repositories. Consider using a repository manager (e.g., Nexus, Artifactory) for better control and caching. * **Don't Do This:** Use insecure HTTP repositories. Declare unnecessary repositories. * **Why This Matters:** Improves build security and performance. Reduces the risk of downloading malicious or compromised dependencies. """gradle // settings.gradle.kts dependencyResolutionManagement { repositories { mavenCentral() gradlePluginPortal() } } """ ### 4.4 Resolution Strategies Leverage Gradle's dependency resolution strategies to handle conflicts and customize dependency resolution. * **Do This:** Use "force" to enforce specific versions, "failOnVersionConflict" to detect conflicts, and "eachDependency" to customize dependency resolution. * **Don't Do This:** Ignore dependency conflicts or allow Gradle to resolve them automatically without understanding the implications. * **Why This Matters:** Provides fine-grained control over dependency resolution and helps prevent runtime errors. """gradle // build.gradle.kts configurations.all { resolutionStrategy { force("org.apache.commons:commons-lang3:3.12.0") failOnVersionConflict() } } """ ## 5. Security Considerations Security should be a primary concern throughout the build process. ### 5.1 Dependency Vulnerability Scanning Integrate dependency vulnerability scanning into your build process. * **Do This:** Use plugins like "org.owasp.dependencycheck" to scan dependencies for known vulnerabilities. * **Don't Do This:** Ignore dependency vulnerabilities or fail to address them promptly. * **Why This Matters:** Helps identify and mitigate security risks associated with vulnerable dependencies. """gradle // build.gradle.kts plugins { id("org.owasp.dependencycheck") version "9.0.9" } dependencyCheck { suppressionFile = "suppressions.xml" } """ ### 5.2 Secure Repository Credentials Protect repository credentials. * **Do This:** Store repository credentials securely (e.g., using environment variables or Gradle properties) and avoid committing them to version control. * **Don't Do This:** Hardcode repository credentials in build scripts or store them in version control. Use masked properties where appropriate. * **Why This Matters:** Prevents unauthorized access to your repositories. """gradle // gradle.properties nexusUsername=myuser nexusPassword=mypassword """ """gradle //build.gradle.kts repositories { maven { url = uri("https://nexus.example.com/repository/maven-releases/") credentials { username = project.properties["nexusUsername"] as String? ?: System.getenv("NEXUS_USERNAME") password = project.properties["nexusPassword"] as String? ?: System.getenv("NEXUS_PASSWORD") } } } """ ### 5.3 Build Script Security Secure your build scripts. * **Do This:** Review build scripts regularly for security vulnerabilities (e.g., command injection, arbitrary code execution). Use static analysis tools to detect potential security issues. Avoid using untrusted third-party plugins. * **Don't Do This:** Execute untrusted build scripts without careful review. * **Why This Matters:** Prevents malicious actors from compromising your build process. ## 6. Testing Practices Comprehensive testing is essential for software quality. ### 6.1 Unit Testing Write comprehensive unit tests for all modules. * **Do This:** Use a testing framework (e.g., JUnit, TestNG, Kotest) and aim for high code coverage. Follow the Arrange-Act-Assert pattern. * **Don't Do This:** Skip unit tests or write tests that are superficial or incomplete. * **Why This Matters:** Catches bugs early, provides confidence in code changes, and facilitates refactoring. """java // src/test/java/com/example/service/GreetingServiceImplTest.java package com.example.service; import com.example.api.GreetingService; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class GreetingServiceImplTest { @Test void greet() { GreetingService greetingService = new GreetingServiceImpl(); String greeting = greetingService.greet("World"); assertEquals("Hello, World!", greeting); } } """ ### 6.2 Integration Testing Write integration tests to verify interactions between modules. * **Do This:** Test the integration of different modules or components. Use testcontainers or similar tools to simulate external dependencies (databases, message queues). * **Don't Do This:** Neglect integration testing, as it can uncover issues that are not apparent in unit tests. * **Why This Matters:** Verifies that different parts of the system work together correctly. ### 6.3 Test Task Configuration Configure the test task appropriately. * **Do This:** Configure JVM arguments, test logging, and other test parameters as needed. Use Gradle's test reporting features to generate test reports. * **Don't Do This:** Use default test task configurations without considering the specific requirements of your project. * **Why This Matters:** Ensures that tests are executed correctly and that test results are easily accessible. """kotlin // build.gradle.kts tasks.test { useJUnitPlatform() jvmArgs("-Xmx256m") testLogging { events("passed", "skipped", "failed") } } """ ## 7. Documentation Good documentation is crucial for long-term maintainability. ### 7.1 Code Documentation Document your code clearly and concisely. * **Do This:** Use Javadoc or KotlinDoc to document classes, methods, and fields. Explain the purpose, usage, and limitations of each element. * **Don't Do This:** Neglect code documentation, as it makes the code harder to understand and maintain. * **Why This Matters:** Improves code understandability, facilitates collaboration, and simplifies maintenance. ### 7.2 Build Script Documentation Document your build scripts. * **Do This:** Explain the purpose of each task, dependency, and configuration setting. Use comments to clarify complex logic. Include a README file with instructions on how to build and run the project. * **Don't Do This:** Write build scripts without any documentation, as it makes them harder to understand and maintain. Use meaningful commit messages documenting changes to the build. * **Why This Matters:** Simplifies build maintenance and ensures that others can understand and modify the build process. ### 7.3 Architecture Documentation Document the overall architecture of your project. * **Do This:** Create diagrams and documents that describe the modules, their dependencies, and the interactions between them. Explain the key design decisions and trade-offs. * **Don't Do This:** Fail to document the architecture, as it makes it harder to understand the big picture and make informed decisions. * **Why This Matters:** Provides a high-level overview of the project and helps ensure that all developers are on the same page. This comprehensive guide covers the core architecture standards for Gradle projects, emphasizing maintainability, performance, and security, using the latest Gradle features and modern best practices. It provides guidance for developers and serves as context for AI coding assistants.
# Component Design Standards for Gradle This document outlines coding standards specifically for component design in Gradle. It focuses on creating reusable, maintainable, and testable components within Gradle builds. Adhering to these standards improves collaboration, reduces code duplication, and enhances the overall quality of Gradle projects. ## 1. Principles of Component Design in Gradle ### 1.1. Single Responsibility Principle (SRP) **Standard:** Each component (plugin, task, extension) should have one, and only one, reason to change. **Why:** SRP enhances maintainability. Changes to one component are less likely to impact others if each component has a well-defined responsibility. **Do This:** * Clearly define the scope of each component before implementation. * Refactor components that perform multiple unrelated tasks into smaller, more focused components. **Don't Do This:** * Create "god" components that handle numerous responsibilities. **Example:** """kotlin // Good: Separate tasks for linting and formatting tasks.register<Detekt>("detekt") { // Linting configuration } tasks.register<KtlintFormatTask>("ktlintFormat") { // Formatting configuration } // Bad: A single task that does both linting and formatting tasks.register("codeQuality") { doLast { // Linting logic // Formatting logic } } """ ### 1.2. Open/Closed Principle (OCP) **Standard:** Components should be open for extension, but closed for modification. **Why:** OCP enables adding new functionality without altering existing code, reducing the risk of introducing bugs. **Do This:** * Use extension points, interfaces, and abstract classes to allow customization. * Create customizable tasks through properties and actions. **Don't Do This:** * Modify existing component code directly to add new features if there are alternative ways. **Example:** """kotlin // Good: Use an extension to configure the plugin open class MyPluginExtension { var message: String = "Hello, world!" } class MyPlugin: Plugin<Project> { override fun apply(project: Project) { val extension = project.extensions.create("myPlugin", MyPluginExtension::class.java) project.task("myTask") { doLast { println(extension.message) } } } } // In build.gradle.kts plugins { id("com.example.myplugin") } myPlugin { message = "Custom message!" } // Bad: Hardcoding configuration directly in the plugin class MyPlugin: Plugin<Project> { override fun apply(project: Project) { project.task("myTask") { doLast { println("Hello, world!") } } } } """ ### 1.3. Liskov Substitution Principle (LSP) **Standard:** Subtypes should be substitutable for their base types without altering the correctness of the program. **Why:** LSP ensures that components can be replaced with their subtypes without causing unexpected behavior. **Do This:** * Always ensure subclasses adhere to the contract defined by the superclass. * Avoid introducing preconditions or postconditions that are stronger than those in the superclass. **Don't Do This:** * Create subclasses that throw exceptions in methods inherited from the superclass, unless they are clearly documented as unsupported operations. **Example:** """kotlin // Good interface ArtifactPublisher { fun publish(artifact: File, repositoryUrl: String) } class MavenArtifactPublisher : ArtifactPublisher { override fun publish(artifact: File, repositoryUrl: String) { // Maven publishing logic } } class IvyArtifactPublisher : ArtifactPublisher { override fun publish(artifact: File, repositoryUrl: String) { // Ivy publishing logic } } // Bad: Introducing exception in subclass that breaks LSP class BrokenArtifactPublisher : ArtifactPublisher { override fun publish(artifact: File, repositoryUrl: String) { throw UnsupportedOperationException("This publisher doesn't support remote repositories") } } """ ### 1.4. Interface Segregation Principle (ISP) **Standard:** Clients should not be forced to depend on methods they do not use. **Why:** ISP promotes loose coupling and prevents unnecessary dependencies. **Do This:** * Break large interfaces into smaller, more specific interfaces. * Provide default implementations for optional methods in interfaces or abstract classes. **Don't Do This:** * Create monolithic interfaces that force clients to implement methods they don't need. **Example:** """kotlin // Good: Separated interfaces for different functionalities interface CodeAnalyzer { fun analyze(file: File) } interface CodeFormatter { fun format(file: File) } class MyCodeAnalyzer : CodeAnalyzer { override fun analyze(file: File) { // Analysis logic } } class MyCodeFormatter : CodeFormatter { override fun format(file: File) { // Formatting logic } } // Bad: A single interface that combines analysis and formatting which might force implementations to provide both even if only one is desired. interface CodeQualityTool { fun analyze(file: File) fun format(file: File) } """ ### 1.5. Dependency Inversion Principle (DIP) **Standard:** High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions. **Why:** DIP promotes loose coupling, improves testability, and makes components more reusable. **Do This:** * Use interfaces or abstract classes to define dependencies. * Employ dependency injection to provide concrete implementations at runtime. **Don't Do This:** * Create direct dependencies from high-level modules to concrete implementations of low-level modules. **Example:** """kotlin // Good: Using dependency injection with constructor injection interface Logger { fun log(message: String) } class ConsoleLogger : Logger { override fun log(message: String) { println(message) } } class MyService(private val logger: Logger) { fun doSomething() { logger.log("Doing something...") } } // In your Gradle task or plugin: val logger = ConsoleLogger() val service = MyService(logger) service.doSomething() // Bad: Directly creating a dependency class MyService { private val logger = ConsoleLogger() fun doSomething() { logger.log("Doing something...") } } """ ## 2. Gradle-Specific Component Design Patterns ### 2.1. Custom Tasks **Standard:** Use custom tasks for encapsulating specific build logic. **Why:** Tasks provide a clear and reusable way to define build actions. **Do This:** * Extend "DefaultTask" or "JavaExec" for custom task implementations. * Annotate task inputs and outputs using "@Input", "@OutputDirectory", "@OutputFile", etc. for incrementality. * Use lazy configuration with "Property" and "Provider" APIs. **Don't Do This:** * Put complex logic directly in "build.gradle.kts" files. * Avoid using the "doLast" and "doFirst" closure configuration style when possible, opting instead for an "Action" object. **Example:** """kotlin import org.gradle.api.DefaultTask import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.RegularFileProperty import org.gradle.api.tasks.Input import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction import org.gradle.api.provider.Property abstract class MyCustomTask : DefaultTask() { @get:Input abstract val message: Property<String> @get:OutputDirectory abstract val outputDir: DirectoryProperty @get:OutputFile abstract val outputFile: RegularFileProperty init { outputDir.convention(project.layout.buildDirectory.dir("customOutput")) outputFile.convention(outputDir.file("output.txt")) } @TaskAction fun execute() { val outputText = "${message.get()} Gradle!" outputFile.get().asFile.writeText(outputText) println("Wrote: $outputText to ${outputFile.get()}") } } tasks.register<MyCustomTask>("myCustomTask") { message.set("Hello") } """ ### 2.2. Custom Plugins **Standard:** Encapsulate reusable build logic into custom plugins. **Why:** Plugins promote modularity and code reuse across multiple projects. **Do This:** * Create plugins using the "Plugin" interface. * Define extension objects to provide configuration options. * Apply plugins programmatically using "project.plugins.apply()". * Publish plugins for wider reuse. **Don't Do This:** * Embed plugin logic directly in "build.gradle.kts" files. * Neglect to apply the 'java-gradle-plugin' in 'java' plugins. **Example:** """kotlin 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", GreetingPluginExtension::class.java) project.task("greeting") { doLast { println("${extension.message.get()} from ${project.name}") } } } } open class GreetingPluginExtension { val message: org.gradle.api.provider.Property<String> = org.gradle.kotlin.dsl.getByType(org.gradle.api.provider.ProviderFactory::class).property(String::class.java).convention("Hello") } // In build.gradle.kts: plugins { id("com.example.greeting") version "1.0.0" } greeting { message.set("Greetings") } """ ### 2.3. Configuration Avoidance **Standard:** Defer the creation and configuration of tasks until they are actually needed. **Why:** Configuration avoidance improves build performance, especially for large projects with many tasks. Tasks that are never needed are never configured, saving time. **Do This:** * Use "tasks.register(...)" instead of "tasks.create(...)" to register tasks lazily. * Use the "named(...)" API to configure existing tasks lazily. * Strongly prefer using providers: "provider { ... }". **Don't Do This:** * Instantiate expensive objects and configurations directly inline. **Example:** """kotlin // Good: Lazy Task Registration and Configuration tasks.register<Copy>("copyDocs") { // Configuration is deferred until the task is needed. from("src/docs") into("${buildDir}/docs") } // Lazy configuration using named: tasks.named<Test>("test") { useJUnitPlatform() } // Bad: Eager Task Creation val copyDocs = tasks.create<Copy>("copyDocs") { from("src/docs") into("${buildDir}/docs") } """ ### 2.4. Convention over Configuration **Standard:** Use Gradle's conventions to reduce explicit configuration. Leverage "buildSrc". **Why:** This simplifies build scripts and makes them easier to understand and maintain. **Do This:** * Follow Gradle's directory structure conventions (e.g., "src/main/java", "src/test/java"). * Use the "buildSrc" directory for custom build logic, extensions, and configurations. **Don't Do This:** * Override conventions without a clear reason. * Copy-paste common configuration across multiple build files. **Example:** """kotlin // Good: Using buildSrc for shared constants // buildSrc/src/main/kotlin/Libs.kt object Libs { const val kotlinVersion = "1.9.22" const val coroutinesVersion = "1.8.0" } // In build.gradle.kts: dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Libs.coroutinesVersion}") } // Bad: Hardcoding versions in multiple build files dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") } """ ### 2.5. Extension Objects **Standard:** Use extension objects to provide configuration options for plugins and tasks. **Why:** Extensions offer a structured and type-safe way to configure components. **Do This:** * Create extension classes using "project.extensions.create()". * Use properties with lazy configuration via "Property" and "Provider" APIs. **Don't Do This:** * Store configuration directly in the task class without an extension. * Use mutable properties like "var" without considering thread safety. **Example:** """kotlin // Good: Extension object with lazy configuration open class MyTaskExtension(objects: ObjectFactory) { val message: Property<String> = objects.property(String::class.java).convention("Default Message") } abstract class MyCustomTask : DefaultTask() { @get:Input abstract val message: Property<String> @TaskAction fun execute() { println(message.get()) } } class MyPlugin : Plugin<Project> { override fun apply(project: Project) { val extension = project.extensions.create("myTaskConfig", MyTaskExtension::class.java, project.objects) val myTask = project.tasks.register<MyCustomTask>("myTask") { message.set(extension.message) } } } // In build.gradle.kts plugins { id("com.example.myplugin") } myTaskConfig { message.set("Custom Message") } // Bad: Direct task configuration without extension tasks.register<MyCustomTask>("myTask") { message.set("Custom Message") // No centralized configuration } """ ## 3. Advanced Component Design Considerations ### 3.1. Incremental Builds **Standard:** Design components to support incremental builds. **Why:** Incremental builds significantly improve build performance by only re-executing tasks with changed inputs. **Do This:** * Annotate task inputs and outputs with appropriate properties like "@Input", "@OutputFile", "@InputDirectory", "@InputFiles", and "@Classpath". * Ensure tasks are cacheable by implementing "org.gradle.workers.WorkAction". Don't use "doLast" or "doFirst" with closures. * Use "@SkipWhenEmpty" or similar annotations if the task should skip execution when certain inputs are empty or missing. **Don't Do This:** * Declare imprecise inputs or outputs, which can cause unnecessary task execution. * Ignore file timestamps or content changes when determining task up-to-dateness. **Example:** """kotlin import org.gradle.api.DefaultTask import org.gradle.api.file.RegularFileProperty import org.gradle.api.tasks.* import org.gradle.api.GradleException abstract class MyTransformTask : DefaultTask() { @get:InputFile @get:PathSensitive(PathSensitivity.RELATIVE) abstract val inputFile: RegularFileProperty @get:OutputFile abstract val outputFile: RegularFileProperty @TaskAction fun transform() { val inputFileVal = inputFile.get().asFile val outputFileVal = outputFile.get().asFile if (!inputFileVal.exists()) { throw GradleException("Input file does not exist: ${inputFileVal.absolutePath}") } println("Transforming ${inputFileVal.name} to ${outputFileVal.name}") outputFileVal.writeText(inputFileVal.readText().toUpperCase()) // Example transformation } } tasks.register<MyTransformTask>("transformFile") { inputFile.set(file("input.txt")) outputFile.set(file("output.txt")) } """ ### 3.2. Worker API **Standard:** Use the Worker API for parallel execution of tasks. **Why:** Allows tasks to perform work concurrently, improving build speed, especially for CPU-bound operations (since Gradle 6.0). **Do This:** * Use "project.workerExecutor" to submit work to the worker queue. * Implement "WorkAction" and associated "WorkParameters" interfaces. * Consider using "IsolationMode.PROCESS" or "IsolationMode.CLASSLOADER" for efficient resource utilization. **Don't Do This:** * Perform I/O operations directly in the task action * Use Thread API's directly. **Example:** """kotlin import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.DirectoryProperty import org.gradle.api.provider.Property import org.gradle.workers.WorkAction import org.gradle.workers.WorkParameters import org.gradle.workers.WorkerExecutor import javax.inject.Inject interface MyWorkParameters : WorkParameters { val inputFile: ConfigurableFileCollection val outputFile: DirectoryProperty val message: Property<String> } abstract class MyWorkAction @Inject constructor() : WorkAction<MyWorkParameters> { override fun execute() { parameters.inputFile.forEach { file -> val outputFile = parameters.outputFile.file(file.name + ".txt").get().asFile outputFile.writeText("${parameters.message.get()} ${file.readText()}") } } } abstract class MyWorkerTask @Inject constructor(private val workerExecutor: WorkerExecutor) : DefaultTask() { @get:InputFiles abstract val inputFiles: ConfigurableFileCollection @get:OutputDirectory abstract val outputDir: DirectoryProperty @get:Input abstract val message: Property<String> @TaskAction fun run() { workerExecutor.noIsolation().submit(MyWorkAction::class.java) { inputFile.from(inputFiles) outputFile.set(outputDir) message.set(message) } } } """ ### 3.3. Task Configuration Avoidance with Providers API **Standard:** Utilize "Provider" APIs for lazy evaluation and configuration of task properties. **Why:** Reduces build time by deferring calculations and configurations until absolutely necessary. Enhances incrementality. **Do This:** * Declare task properties as "Property<T>" and configure them using "Provider<T>". * Use "map()", "flatMap()", and "orElse()" methods to transform and combine providers. * Use "provider { ... }" block to create providers from computations that should be executed only when the value is needed. **Don't Do This:** * Access task properties directly with ".get()" during configuration, forcing early evaluation. **Example:** """kotlin import org.gradle.api.provider.Provider abstract class MyConfigurableTask : DefaultTask() { @get:Input abstract val version: Property<String> init { //Example of configuring the value of "version" to use the project's version version.convention(project.provider { project.version.toString() }) } @TaskAction fun execute() { println("Version: ${version.get()}") } } """ ## 4. Security Considerations ### 4.1. Secrets Management **Standard:** Avoid hardcoding secrets in build scripts. **Why:** Prevents accidental exposure of sensitive information **Do This:** * Use environment variables or Gradle properties to store secrets. * Retrieve secrets using "System.getenv()" or "project.properties". * Consider using a dedicated secrets management tool like HashiCorp Vault. * Use "buildSrc" to store constants. **Don't Do This:** * Commit secrets directly to source control. * Pass secrets as command-line arguments. **Example:** """kotlin // Retrieving secret from environment variable val apiKey = System.getenv("API_KEY") ?: "defaultApiKey" tasks.register("apiCall") { doLast { println("Using API key: $apiKey") } } """ ### 4.2. Dependency Management **Standard:** Manage dependencies securely. **Why:** Mitigates risks associated with vulnerable dependencies. **Do This:** * Use dependency management features like BOMs (Bill of Materials) to ensure consistent versions. * Use dependency verification to ensure that artifacts come from trusted sources. * Employ dependency scanning tools (e.g., OWASP Dependency-Check) to identify vulnerabilities. * Regularly update dependencies to the latest secure versions. **Don't Do This:** * Use wildcard versions (e.g., "1.+"). * Ignore security vulnerabilities reported by dependency scanning tools. **Example:** """kotlin dependencies { implementation(platform("org.springframework.boot:spring-boot-dependencies:3.3.0")) // Use BOM implementation("org.springframework.boot:spring-web") // Explicitly declaring the library uses the BOM version. constraints { implementation("org.apache.logging.log4j:log4j-core") { version { strictly("[2.22.1]") // Specify version in a security constraint } because("Addresses CVE-2024-27958") } } } """ ## 5. Conclusion These coding standards for component design in Gradle provide a foundation for creating high-quality, maintainable, and secure builds. Adhering to these principles and best practices will improve team collaboration, reduce technical debt, and enhance the overall success of Gradle projects. Regularly review and update these standards to incorporate new Gradle features and evolving best practices.
# Testing Methodologies Standards for Gradle This document outlines the testing methodologies standards for Gradle projects. Following these standards will lead to more reliable, maintainable, and performant builds. It covers unit, integration, and end-to-end testing strategies, specifically within the context of Gradle. ## 1. General Testing Principles ### 1.1. Test-Driven Development (TDD) * **Do This:** Embrace TDD by writing tests *before* implementing the actual code. This helps clarify requirements, reduces bugs, and leads to better design. * **Don't Do This:** Write tests as an afterthought or skip them altogether. This increases the risk of bugs and makes refactoring harder. * **Why:** TDD ensures that the code is testable from the start, leading to better code coverage and reduced technical debt. ### 1.2. Test Pyramid * **Do This:** Follow the Test Pyramid, with a large base of unit tests, a smaller layer of integration tests, and a very small number of end-to-end (E2E) tests. * **Don't Do This:** Rely heavily on E2E tests at the expense of unit and integration tests. E2E tests are slow and brittle. * **Why:** The Test Pyramid ensures a balanced testing strategy, optimizing for speed, cost, and coverage. Unit tests provide fast feedback, integration tests verify interactions, and E2E tests validate the system as a whole. ### 1.3. Test Coverage * **Do This:** Aim for high test coverage (e.g., 80% or higher) but focus on meaningful tests that cover critical functionality and edge cases. * **Don't Do This:** Strive for 100% coverage without considering the quality of the tests. Coverage is a metric, not a goal in itself. * **Why:** Test coverage provides a measure of how much of the codebase is covered by tests. High coverage, combined with well-written tests, increases confidence in the code's correctness. ### 1.4. Test Independence * **Do This:** Ensure that tests are independent of each other. Each test should set up its own environment and tear it down afterward. * **Don't Do This:** Allow tests to depend on the state left by previous tests. This can lead to flaky tests and make debugging difficult. * **Why:** Independent tests make it easier to reason about individual tests, reduce the risk of cascading failures, and allow tests to be run in parallel. ### 1.5. Clear Assertions * **Do This:** Write clear and specific assertions that clearly state what is being tested. Use descriptive error messages when assertions fail. * **Don't Do This:** Use generic assertions or assertions that are difficult to understand. This makes it harder to diagnose the cause of test failures. * **Why:** Clear assertions make it easier to understand the purpose of each test and to quickly identify the source of errors. ## 2. Unit Testing with Gradle ### 2.1. Using JUnit and Mockito * **Do This:** Use JUnit 5 as the standard unit testing framework and Mockito for mocking dependencies. """gradle dependencies { testImplementation("org.junit.jupiter:junit-jupiter-api:5.11.0-M1") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.11.0-M1") testImplementation("org.mockito:mockito-core:5.11.0") testImplementation("org.mockito:mockito-junit-jupiter:5.11.0") } test { useJUnitPlatform() } """ * **Don't Do This:** Rely on older versions of JUnit or use custom mocking frameworks without a strong justification. * **Why:** JUnit 5 is the latest version of JUnit and provides a rich set of features for writing and running unit tests. Mockito is a popular mocking framework that simplifies the creation of mock objects. ### 2.2. Structure of Unit Tests * **Do This:** Follow the AAA (Arrange, Act, Assert) pattern in unit tests. * **Arrange:** Set up the environment for the test (e.g., create objects, configure mocks). * **Act:** Execute the code being tested. * **Assert:** Verify that the code behaves as expected. * **Don't Do This:** Mix the Arrange, Act, and Assert sections or perform setup within the assertion. * **Why:** The AAA pattern makes tests more readable and easier to understand. """java import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; class ExampleServiceTest { @Test void testCalculateSum() { // Arrange Calculator calculator = mock(Calculator.class); when(calculator.add(2, 3)).thenReturn(5); ExampleService service = new ExampleService(calculator); // Act int result = service.calculateSum(2, 3); // Assert assertEquals(5, result, "The sum should be 5"); verify(calculator).add(2, 3); // Verify the method was called } } class ExampleService { private Calculator calculator; public ExampleService(Calculator calculator) { this.calculator = calculator; } public int calculateSum(int a, int b) { return calculator.add(a, b); } } interface Calculator { int add(int a, int b); } """ ### 2.3. Mocking Strategies * **Do This:** Use Mockito annotations ("@Mock", "@InjectMocks") to simplify mock creation and injection. """java import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class ExampleServiceTest { @Mock private Calculator calculator; @InjectMocks private ExampleService service; @Test void testCalculateSum() { when(calculator.add(2, 3)).thenReturn(5); int result = service.calculateSum(2, 3); assertEquals(5, result, "The sum should be 5"); verify(calculator).add(2, 3); } } """ * **Don't Do This:** Use manual mock creation and injection, which can be verbose and error-prone. Over-mocking - mock only external dependencies, not the class under test itself. * **Why:** Mockito annotations reduce boilerplate code and make tests more readable. ### 2.4. Parameterized Tests * **Do This:** Use JUnit 5's "@ParameterizedTest" to run the same test with different input values. """java import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import static org.junit.jupiter.api.Assertions.*; class StringUtilsTest { @ParameterizedTest @CsvSource({ "apple, APPLE", "banana, BANANA", "cherry, CHERRY" }) void testConvertToUpperCase(String input, String expected) { assertEquals(expected, StringUtils.convertToUpperCase(input)); } } class StringUtils { static String convertToUpperCase(String input) { return input.toUpperCase(); } } """ * **Don't Do This:** Write separate tests for each input value, which can lead to code duplication. * **Why:** Parameterized tests reduce code duplication and make it easier to test multiple scenarios. ### 2.5. Gradle Test Task Configuration * **Do This:** Configure the Gradle test task to fail the build if tests fail. Consider setting JVM arguments for the test execution. """gradle test { useJUnitPlatform() testLogging { events "passed", "skipped", "failed" exceptionFormat "full" } jvmArgs "-Xmx256m" // Set maximum heap size } """ * **Don't Do This:** Ignore test failures or skip tests during the build. * **Why:** Failing the build on test failures ensures that broken code is not deployed. Providing test logging helps to understand the tests execution and potential failures. ## 3. Integration Testing with Gradle ### 3.1. Purpose of Integration Tests * **Do This:** Write integration tests to verify the interactions between different modules or components of the system. Focus on testing the "seams" between different parts of the application. * **Don't Do This:** Use integration tests to test individual units of code. That's the job of unit tests. * **Why:** Integration tests ensure that the different parts of the system work together correctly. ### 3.2. Testing External Dependencies * **Do This:** Use test containers (e.g., Docker containers) to provide a consistent and isolated environment for integration tests that depend on external services (e.g., databases, message queues). """gradle dependencies { testImplementation("org.testcontainers:testcontainers:1.19.6") testImplementation("org.testcontainers:junit-jupiter:1.19.6") testImplementation("org.testcontainers:postgresql:1.19.6") } """ * **Don't Do This:** Rely on shared or production environments for integration tests, which can lead to inconsistent results and data corruption. * **Why:** Test containers provide a reliable and reproducible environment for integration tests. """java import org.junit.jupiter.api.Test; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import java.sql.*; import static org.junit.jupiter.api.Assertions.assertTrue; @Testcontainers class DatabaseIntegrationTest { @Container private static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine") .withDatabaseName("testdb") .withUsername("test") .withPassword("test"); @Test void testDatabaseConnection() throws SQLException { String jdbcUrl = postgres.getJdbcUrl(); String username = postgres.getUsername(); String password = postgres.getPassword(); try (Connection connection = DriverManager.getConnection(jdbcUrl, username, password)) { assertTrue(connection.isValid(5), "Connection should be valid"); } } } """ ### 3.3. Mocking External Services * **Do This:** Use mock servers (e.g., WireMock, MockServer) to simulate the behavior of external services that are not available during integration testing. * **Don't Do This:** Make real calls to external services during integration tests, which can be slow, unreliable, and costly. However, avoid over-mocking. Test the actual interactions when reasonable. ### 3.4. Database Testing * **Do This:** Use a dedicated test database and populate it with test data before running integration tests that interact with a database. * **Don't Do This:** Use the production database or modify shared data during integration tests. * **Why:** Using a dedicated test database prevents data corruption. ### 3.5. Transactional Tests * **Do This:** Wrap integration tests that modify data in a transaction and roll back the transaction after the test completes. * **Don't Do This:** Leave data modifications in the database after running integration tests. * **Why:** Transactional tests ensure that the database remains in a consistent state after running tests. ## 4. End-to-End (E2E) Testing with Gradle ### 4.1. Purpose of E2E Tests * **Do This:** Write E2E tests to verify the complete system workflow from the user's perspective. Focus on testing the most critical user journeys. * **Don't Do This:** Use E2E tests to test individual components or units of code. That's the job of unit and integration tests. * **Why:** E2E tests provide the highest level of confidence that the system works as expected. ### 4.2. Automation Frameworks * **Do This:** Use an automation framework (e.g., Selenium, Playwright, Cypress) to automate E2E tests. * **Don't Do This:** Manually run E2E tests, which can be time-consuming and error-prone. * **Why:** Automation frameworks make E2E tests more efficient and reliable. ### 4.3. Test Environments * **Do This:** Run E2E tests in a dedicated test environment that closely resembles the production environment. * **Don't Do This:** Run E2E tests in development environments, which may not be representative of the production environment. * **Why:** Using a dedicated test environment ensures that E2E tests are run in a realistic environment. ### 4.4. Browser Management * **Do This:** Use a browser management tool (e.g., WebDriverManager) to automatically download and manage browser drivers for E2E tests. * **Don't Do This:** Manually download and configure browser drivers, which can be a tedious and error-prone process. * **Why:** Browser management tools simplify the configuration of E2E tests. ### 4.5. Test Data Management * **Do This:** Use a consistent and reliable strategy for managing test data in E2E tests. This may involve creating test data programmatically or using a test data generation tool. * **Don't Do This:** Rely on hardcoded test data or manual data entry, which can be inconsistent and unreliable. * **Why:** Proper test data management ensures that E2E tests are run with consistent and realistic data. ## 5. Gradle and Continuous Integration ### 5.1. Integrating Tests into CI/CD Pipeline * **Do This:** Configure your CI/CD pipeline to automatically run all tests (unit, integration, and E2E) on every commit. * **Don't Do This:** Skip running tests in the CI/CD pipeline, which can lead to broken code being deployed to production. * **Why:** Integrating tests into the CI/CD pipeline provides continuous feedback on the quality of the code. ### 5.2. Parallel Test Execution * **Do This:** Configure Gradle to run tests in parallel to reduce the overall test execution time. """gradle test { maxParallelForks = (Runtime.runtime.availableProcessors() / 2) ?: 1 // Use half the available processors } """ * **Don't Do This:** Run tests sequentially, which can be slow and inefficient. Ensure your tests are written to support parallel execution (e.g., no shared mutable state). * **Why:** Parallel test execution can significantly reduce the time it takes to run the test suite. ### 5.3. Test Reporting * **Do This:** Configure Gradle to generate detailed test reports (e.g., HTML reports) that show the results of each test. Then, integrate the publication of test reports to common CI systems like Jenkins and Github Actions. """gradle test { useJUnitPlatform() reports { junitXml.required = true html.required = true } } """ * **Don't Do This:** Rely on console output to understand the results of tests, which can be difficult to parse and analyze. * **Why:** Test reports provide a clear and concise overview of the test results, making it easier to identify and fix failing tests. ### 5.4. Flaky Test Management * **Do This:** Implement a strategy for identifying and managing flaky tests (tests that sometimes pass and sometimes fail). This may involve re-running flaky tests multiple times or disabling them temporarily. * **Don't Do This:** Ignore flaky tests, which can undermine confidence in the test suite. * **Why:** Flaky tests can lead to false positives and mask real bugs. ### 5.5 Caching Test Results * **Do This:** Enable Gradle's build cache to reuse test outputs between builds, especially in CI environments. This is achieved by ensuring build tasks are properly configured for caching. * **Don't Do This:** Disable or misconfigure the build cache, leading to unnecessary test re-executions and slower build times. ## 6. Common Anti-Patterns * **Ignoring Test Failures:** Never ignore test failures or skip tests; always investigate and fix them. * **Writing Untestable Code:** Design code with testability in mind; avoid tight coupling and hidden dependencies. * **Over-Mocking:** Use mocks judiciously; avoid mocking everything, as it can make tests brittle and less valuable. Test actual interactions when possible. * **Not Cleaning Up After Tests:** Ensure that tests clean up any data or state they create, to avoid interference with other tests. * **Long Setup:** Keep setup as minimal as possible, using helper functions to instantiate objects and mocks. Aim for speed and readability. * **Complex Assertions:** Structure assertions for readability, using Hamcrest matchers or similar libraries to write simple-to-understand validations. ## 7. Security Considerations * **Avoid Storing Secrets in Tests:** Don't hardcode sensitive information (API keys, passwords) in test files. Use environment variables or secure configuration mechanisms. * **Secure Test Environments:** Protect test environments from unauthorized access and ensure data is properly secured and anonymized where needed. * **Regular Security Audits:** Review testing practices and configurations regularly to ensure they align with security best practices. By adhering to these standards, Gradle projects can benefit from improved code quality, reduced bug rates, and enhanced maintainability.