Declaring dependencies

Gradle · Essentials · 10 min

Most build changes are dependency changes; using the right configuration keeps compile classpaths lean and APIs clean.

Dependencies are declared in the dependencies { } block as "group:name:version" coordinates, assigned to a configuration that says when they are needed:

  • implementation: needed to compile and run your code, but not exposed to consumers of your library. The default choice.
  • api (from the java-library plugin): part of your public API; consumers get it on their compile classpath too.
  • compileOnly: available at compile time only (annotations, provided servers). runtimeOnly: only at run time (JDBC drivers, logging backends).
  • testImplementation, testRuntimeOnly: the same for tests.
  • annotationProcessor for tools like Lombok or MapStruct.

Gradle downloads artifacts from the repositories { } you list (mavenCentral(), google(), mavenLocal(), or a company Nexus/Artifactory) and caches them in ~/.gradle/caches.

Transitive dependencies come along automatically. When two paths need different versions of the same library, Gradle picks the highest version by default (not "nearest" like Maven). See the result with ./gradlew dependencies or ./gradlew dependencyInsight --dependency jackson-databind.

Dynamic versions (1.+) and latest.release make builds non-reproducible; prefer exact versions. Rich versions (strictly, prefer) and constraints { } adjust transitive versions without declaring a direct dependency. Project dependencies use implementation(project(":lib")).

Example

plugins { `java-library` }

repositories { mavenCentral() }

dependencies {
    api("com.fasterxml.jackson.core:jackson-databind:2.18.3")   // leaks into consumers' compile classpath
    implementation("org.slf4j:slf4j-api:2.0.17")              // internal
    compileOnly("org.projectlombok:lombok:1.18.36")
    annotationProcessor("org.projectlombok:lombok:1.18.36")
    runtimeOnly("ch.qos.logback:logback-classic:1.5.18")      // logging backend, not compiled against
    implementation(project(":core"))                          // another subproject

    constraints {
        implementation("com.google.guava:guava:33.4.0-jre") { because("CVE fix") }
    }
    testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
}

// $ ./gradlew dependencies --configuration compileClasspath
// $ ./gradlew dependencyInsight --dependency slf4j-api

Remember

  • implementation hides a dependency from consumers; api exposes it
  • compileOnly/runtimeOnly split compile and run time
  • Gradle picks the highest version among conflicting transitive versions
  • dependencyInsight explains why a version was chosen

Watch out for

  • Using api for everything, which drags your internals onto every consumer's compile classpath.

Check yourself

  1. A library you build uses Jackson only inside its implementation. Which configuration should you use?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.gradle.org/current/userguide/declaring_dependencies.html