skip to content

What is the DependencyHandler.add(configuration, notation) API, and how does it relate to the `implementation(...)` DSL methods?

level: middleimportance: should knowfreq 45%

answer

  1. add(configurationName, notation)
  2. DSL methods = generated sugar over add
  3. per-configuration method auto-created
  4. use add for dynamic/computed config names
  5. 3-arg overload for configuring closure

basics

~10 s

add(configuration, notation) is the underlying DependencyHandler method that attaches a dependency to a named configuration. The implementation(...), api(...) etc. DSL calls are just convenience wrappers that call add with that configuration name.

solid answer

~40 s

Inside `dependencies { }` you're operating on a **`DependencyHandler`**. Its general method is `add(String configurationName, Object dependencyNotation)` — it takes the target configuration's name and any supported notation (string `g:n:v`, a map, a project, files, a platform, etc.), creates the corresponding `Dependency` object, and registers it on that configuration. The familiar `implementation("g:n:v")`, `testImplementation(...)`, `api(...)` calls are **dynamically-provided convenience methods**: each configuration contributes a method of the same name that delegates to `add("<thatConfiguration>", notation)`. You reach for `add` directly when the configuration name is computed/dynamic, when you iterate over configurations programmatically, or in plugin code where the typed DSL method may not exist. There's also an overload `add(configuration, notation, configureClosure)` to set per-dependency options. So the DSL methods are sugar; `add` is the API they're sugar over.

code

kotlin · 9 lines
kotlin
dependencies {
    // these two are equivalent
    implementation("com.google.guava:guava:33.0.0-jre")
    add("implementation", "com.google.guava:guava:33.0.0-jre")

    // useful when the configuration name is computed
    val cfg = "testImplementation"
    add(cfg, "org.assertj:assertj-core:3.25.3")
}

go deeper

for a junior

Just recognize that implementation(...) adds a dependency to the implementation configuration.

for a middle

Explain that add(configuration, notation) is the underlying API and that DSL methods are generated per configuration as sugar over it.

for a senior

Show the dynamic/looping use cases and the three-arg overload; tie it to how custom configurations gain their own DSL method.

for a principal

Discuss plugin-authoring patterns, programmatic dependency wiring across configurations, and keeping such logic readable/maintainable in a shared build platform.

## The object behind `dependencies { }` The `dependencies { }` block configures a project's **`DependencyHandler`**. Every way of declaring a dependency ultimately funnels through it. ## The core method ``` Dependency add(String configurationName, Object dependencyNotation) Dependency add(String configurationName, Object dependencyNotation, Closure configureClosure) ``` - **`configurationName`** — the name of the configuration to attach to (`"implementation"`, `"api"`, `"testRuntimeOnly"`, or a custom one you created). - **`dependencyNotation`** — any supported notation: a `"g:n:v"` string, a map, the result of `project(":other")`, `files(...)`, `platform(...)`, etc. - It builds the matching `Dependency` instance and adds it to that configuration, returning the created dependency. ## Why the DSL methods feel different When a plugin creates a configuration (the `java` plugin creates `implementation`, `api`, `compileOnly`, `runtimeOnly`, `testImplementation`, …), Gradle exposes a **same-named method** on the `DependencyHandler` so you can write: ```kotlin dependencies { implementation("com.google.guava:guava:33.0.0-jre") } ``` instead of: ```kotlin dependencies { add("implementation", "com.google.guava:guava:33.0.0-jre") } ``` Both lines are equivalent — the first is generated sugar over the second. ## When to call `add` directly - **Dynamic configuration names** — the target is computed at runtime: ```kotlin val cfg = if (useNative) "runtimeOnly" else "implementation" dependencies { add(cfg, "io.netty:netty-transport-native-epoll:4.1.107.Final") } ``` - **Iterating/programmatic registration** — e.g. a loop or plugin adding the same dependency to several configurations. - **Plugin authoring** — where you don't have (or don't want to rely on) the typed accessor. ## With a configuring block The three-arg overload lets you set options on the created dependency: ```kotlin dependencies { add("implementation", "org.hibernate:hibernate-core:6.4.4.Final") { this as ExternalModuleDependency isTransitive = false } } ``` ## Mental model Think of `add` as the single registration primitive and the per-configuration methods (`implementation`, `api`, …) as thin, ergonomic aliases generated per configuration. Knowing this demystifies how custom configurations get their own DSL method and unlocks dynamic/looping declaration patterns.

  • Where does the `implementation(...)` method come from if it isn't hand-written in the API?
    It's contributed dynamically when the configuration `implementation` is created (by the java/java-library plugin); Gradle exposes a same-named method on the DependencyHandler that delegates to `add("implementation", ...)`.
  • When is calling `add` directly clearly better than the DSL method?
    When the configuration name is computed at runtime, when looping to register dependencies across multiple configurations, or in plugin code that can't rely on a typed accessor.

saying these in an interview costs you the question

  • Believing `implementation` is a special hard-coded keyword rather than a per-configuration generated method over `add`.
  • Thinking `add` and the DSL methods produce different resolution behavior.

context