skip to content

When automatic substitution in a composite build isn't enough, how do you declare an explicit substitution, and what is the syntax?

level: middleimportance: must knowfreq 50%

answer

  1. dependencySubstitution block on includeBuild
  2. substitute(module(...)).using(project(...))
  3. project path is inside the included build
  4. .using replaced deprecated .with
  5. fixes coordinate mismatch

basics

~10 s

Use a dependencySubstitution block on the includeBuild and call substitute(module("group:name")).using(project(":path")) to map a published module to a specific project in the included build.

solid answer

~30 s

Automatic substitution covers the common case, but you need **explicit substitution** when the consumer's declared coordinates don't match what the included build publishes — for example when `group`/`name` differ, when only some configurations should substitute, or when you want to be precise about which project handles which module. You attach a `dependencySubstitution { ... }` block directly to the `includeBuild` call in `settings.gradle.kts` and write `substitute(module("com.acme:widgets")).using(project(":widgets-core"))`. The `module(...)` is the external coordinate the consumer asks for; `project(...)` is the path **within the included build**. This overrides/augments automatic detection and is also how you fix the silent-fallback problem when coordinates were mismatched.

code

kotlin · 10 lines
kotlin
// settings.gradle.kts of the consumer
includeBuild("../widgets-lib") {
    dependencySubstitution {
        // consumer asks for com.acme:widgets-api -> satisfy from local :api project
        substitute(module("com.acme:widgets-api"))
            .using(project(":api"))
        substitute(module("com.acme:widgets-impl"))
            .using(project(":impl"))
    }
}

go deeper

for a junior

Know that an explicit block exists for when coordinates don't line up automatically.

for a middle

Write the full substitute(module(...)).using(project(...)) syntax and place it on the includeBuild in settings.

for a senior

Explain when explicit is required over automatic, and that the project path is included-build-relative.

for a principal

Treat explicit mappings as part of a curated cross-repo dev workflow, documenting coordinate drift and migration paths.

## Why explicit substitution exists Automatic substitution only fires when the included build's published `group:name` exactly equals what the consumer requested. Real codebases drift: a library was renamed, the artifact id differs from the project name, or coordinates were never aligned. **Explicit dependency substitution** lets you state the mapping by hand. ## The DSL You attach the rule to the `includeBuild` call itself in `settings.gradle.kts`: ```kotlin includeBuild("../widgets-lib") { dependencySubstitution { substitute(module("com.acme:widgets")) .using(project(":widgets-core")) } } ``` Reading it: *"any external dependency on `com.acme:widgets` should be satisfied by the project at path `:widgets-core` inside `../widgets-lib`."* Key pieces: - `module("group:name")` — the **selector**: the external coordinate the consumer declares. You may also include a version, but for composites you usually omit it so any requested version matches. - `using(project(":path"))` — the **target**: a project **path relative to the included build's root**, not the consumer. - `.using(...)` replaced the older deprecated `.with(...)` form (removed in Gradle 8). Don't say `with`. ## When you need it - Artifact id ≠ project name (`module("com.acme:widgets-api").using(project(":api"))`). - The included build publishes under a different group than the consumer requests. - You want to substitute only a specific module while leaving others to the repository. - Migrating a multi-module library where automatic detection picks the wrong project. ## Verifying Always confirm with `./gradlew :app:dependencyInsight --dependency com.acme:widgets`; a working substitution shows `-> project :widgets-core`.

  • Is the project path in `using(project(...))` relative to the consumer or the included build?
    Relative to the included build's root. `:api` means the `api` subproject of `../widgets-lib`, not of the consuming build.
  • What's the deprecated method this replaced?
    `.with(...)` — replaced by `.using(...)`; the old form was removed, so on Gradle 8 you must use `using`.
  • Can you omit the version in `module(...)`?
    Yes, and for composites you usually do, so any requested version of those coordinates is substituted to the local project.

saying these in an interview costs you the question

  • Using `.with(project(...))` — deprecated and removed in Gradle 8.
  • Interpreting the project path as relative to the consuming build.
  • Putting the dependencySubstitution block in build.gradle instead of on the includeBuild call in settings.

context