Gradle Dependencies & Tasks
Gradle Dependencies & Tasks
Gradle's power lies in two interlocking systems: its dependency management model — built around named configurations — and its task graph, which lets you compose, extend, and automate every aspect of your build. In this lesson you will master both at the depth required for production projects.
Dependency Configurations — the Full Picture
In Gradle, a configuration is a named bucket that groups dependencies for a specific purpose (compiling, running tests, generating annotation processors, etc.). The Java plugin and the application/library plugins register several configurations automatically.
The most important ones for a modern Java project are:
implementation— the dependency is on the compile classpath of this module but is not leaked to consumers that depend on this module. Use it for the vast majority of your dependencies.api(library plugin only) — likeimplementationbut does expose the dependency to consumers. Use sparingly: overusingapiforces unnecessary recompilation across the entire project graph.compileOnly— present during compilation, absent from the runtime classpath. Classic use-case: Lombok, Jakarta annotation APIs when the runtime container provides them.runtimeOnly— absent at compile time, present at runtime. Typical use-case: JDBC drivers, SLF4J bindings (e.g.logback-classic).testImplementation— likeimplementationbut scoped to the test source set.testCompileOnly/testRuntimeOnly— test-scoped equivalents of the above.annotationProcessor— annotation processors run byjavac; they are NOT placed on the application classpath at all.
implementation vs api matter so much? Gradle tracks which jars appear on the compile classpath of downstream projects. If you use api, a change in that transitive dependency triggers recompilation of every downstream module. In a large multi-module build this can add minutes to incremental builds. Prefer implementation by default and only promote to api when the type is part of your public API surface.
Declaring Dependencies
Dependencies are declared inside the dependencies { } block in build.gradle (Groovy) or build.gradle.kts (Kotlin DSL). The canonical form is the Maven coordinate: group:artifact:version.
platform(...) and omit version strings on individual dependencies. Spring Boot's BOM is the canonical example.
Dependency Resolution and Version Conflict Strategies
When the dependency graph contains the same library at two different versions, Gradle applies optimistic version selection by default: it picks the highest requested version. This differs from Maven's nearest-wins strategy and typically produces a more correct result.
You can override resolution behaviour when needed:
Custom Tasks — Fundamentals
Every action in a Gradle build is a task. A task has inputs, outputs, and a set of actions. Gradle's incremental build system uses the inputs/outputs to determine whether a task is UP-TO-DATE and can be skipped entirely — this is the primary source of Gradle's speed advantage over Maven for large projects.
The simplest way to define a custom task is using the tasks.register API, which creates the task lazily (it is not configured until something actually needs it):
Run it with ./gradlew printBuildInfo. The group and description fields make the task discoverable via ./gradlew tasks --all.
Typed Tasks — the Professional Approach
Most real tasks extend a built-in type such as Copy, Jar, JavaExec, Exec, or Zip. Typed tasks get incremental build support automatically when you declare inputs and outputs correctly.
Task Dependencies and Ordering
Tasks form a directed acyclic graph (DAG). You control relationships with three mechanisms:
dependsOn— ensures the named task runs before this one and actually runs it.mustRunAfter— enforces ordering when both tasks are scheduled but does not trigger the other task.finalizedBy— runs a task after this one, even if this task fails (useful for teardown / test-report generation).
tasks.create (eager registration). It configures the task immediately at configuration time — every time the build script is evaluated — even if the task is never going to run. With dozens of custom tasks this wastes seconds on every Gradle invocation. Always use tasks.register (lazy) instead.
Incremental Builds and Caching
For custom tasks to be incremental, declare your inputs and outputs explicitly using Gradle's property annotations or the inputs/outputs API:
With @Input and @OutputFile in place, Gradle will skip the task if the version string has not changed since the last build — and with the build cache enabled (org.gradle.caching=true in gradle.properties), it can even restore the output from a remote cache shared across CI agents, yielding near-zero rebuild times for unchanged work.
Summary
Dependency configurations (implementation, api, compileOnly, runtimeOnly, annotationProcessor) give you precise control over what appears on each classpath and what is exposed to consumers. Custom tasks — registered lazily with tasks.register, typed for incremental support, and linked via dependsOn/finalizedBy — let you extend the build with arbitrary automation while keeping it fast. Declare inputs and outputs accurately and Gradle's incremental and caching machinery does the rest.