skip to content

Exclusions and Transitivity Control

Per-dependency and configuration-wide excludes, disabling transitivity entirely, and documenting a pin with because(). Interviewers ask because an exclude with no stated reason is the build equivalent of an unexplained hack.

on this pageshow

questions

5

How do you exclude a single transitive dependency that is pulled in by one specific dependency in Gradle?

level: juniorimportance: must knowfreq 70%

answer

  1. exclude inside the dependency closure
  2. group and/or module — never version
  3. scoped to one dependency's subgraph
  4. other paths can still pull it in
  5. most surgical option

basics

~10 s

Attach an exclude block to that one dependency, naming the unwanted artifact by group and/or module. Only that dependency's transitive graph is affected.

solid answer

~40 s

Use the per-dependency `exclude` form so the exclusion is scoped to one declared dependency rather than the whole project. In the dependencies block you call `implementation("com.acme:foo:1.0") { exclude(group = "org.slf4j", module = "slf4j-simple") }`. Both keys are optional: `group` alone drops every module from that group brought in transitively by `foo`; `module` alone drops that module regardless of group; together they pin one coordinate. This is the most surgical exclusion — it does not touch what other dependencies bring in, so if a second library also pulls `slf4j-simple`, it survives. Prefer this over broad rules when only one path is problematic, and document why with a comment or a `because(...)` reason so the next person understands the intent.

code

kotlin · 8 lines
kotlin
dependencies {
    implementation("com.acme:foo:1.0") {
        // drop one coordinate
        exclude(group = "org.slf4j", module = "slf4j-simple")
        // drop a whole group transitively pulled by foo
        exclude(group = "commons-logging")
    }
}

go deeper

for a junior

Know the syntax: exclude(group, module) inside the dependency block, and that group/module are the keys.

for a middle

Explain the scoping: it only affects this dependency's transitive subgraph, so duplicate paths can still bring it in.

for a senior

Contrast per-dependency vs configuration-wide excludes and pick the right granularity; pair with because() for documentation.

for a principal

Set team conventions: prefer constraints/platform alignment over scattered excludes, and require documented reasons so the graph stays auditable.

## What "transitive" means When you declare a dependency like `com.acme:foo:1.0`, Gradle reads its published metadata (a Gradle Module Metadata `.module` file or a Maven `.pom`) and automatically resolves everything `foo` itself depends on. Those second-, third-, ... order dependencies are **transitive dependencies**. They make adding a library convenient, but they sometimes drag in things you don't want: a conflicting logging backend, a vulnerable version, or a duplicate of something you already provide. ## Per-dependency exclude The narrowest tool is an `exclude` attached to a *single* dependency declaration: - `exclude(group = "...")` — drop everything from that Maven group that **this** dependency pulls in transitively. - `exclude(module = "...")` — drop that module name regardless of group. - both — pin exactly one `group:module` coordinate. Note you exclude by **group and module, never by version** — Gradle removes the node from the graph entirely; you don't say "exclude version 1.2". ## Scope The key property is **scope**: the rule only prunes the subgraph reachable *through that one declared dependency*. If module `bar` (declared separately) also brings the excluded artifact, it still arrives. That makes per-dependency exclude precise but also means it can silently fail to remove an artifact when multiple paths supply it. ## When to reach for it Use it when exactly one library is responsible for the unwanted transitive — e.g. a library that bundles `slf4j-simple` while your app uses Logback. For a project-wide problem (the same junk arrives through many paths), a `configurations.all { exclude(...) }` rule is more appropriate. ```kotlin dependencies { implementation("com.acme:foo:1.0") { exclude(group = "org.slf4j", module = "slf4j-simple") because("foo bundles slf4j-simple, but we log through Logback") } } ```

  • If two libraries each transitively pull slf4j-simple and you only exclude it on one, what happens?
    It still ends up on the classpath via the second library. Per-dependency exclude only prunes the subgraph of the dependency it's attached to; you'd need to exclude on both, or use a configuration-wide rule.
  • Can you exclude by version?
    No. Excludes operate on group/module coordinates and remove the node entirely. To force a specific version you use a different mechanism (a constraint or `resolutionStrategy.force` / strict version).

saying these in an interview costs you the question

  • Claiming exclude takes a version argument.
  • Assuming a per-dependency exclude removes the artifact globally even when another dependency supplies it.

context

open as a page

When would you use `configurations.all { exclude(...) }` instead of a per-dependency exclude, and what are the trade-offs?

level: middleimportance: should knowfreq 55%

basics

~10 s

Use a configuration-wide exclude when an unwanted artifact arrives through many dependency paths. It removes that coordinate from every dependency in the configuration at once, but it's a blunt, global rule.

open as a page

What does `isTransitive = false` do on a dependency, and how does it differ from excludes?

level: middleimportance: should knowfreq 45%

basics

~10 s

Setting isTransitive = false makes Gradle bring in only that one artifact and none of its declared dependencies. Excludes prune specific coordinates; isTransitive = false drops the entire transitive subtree at once.

open as a page

Excludes are often a code smell. When are they justified, and what modern Gradle mechanisms should you prefer for managing the transitive graph?

level: seniorimportance: should knowfreq 35%

basics

~10 s

Excludes are justified for genuinely unwanted artifacts (duplicate log backends, broken metadata). For version conflicts prefer constraints/platforms; for competing implementations of the same capability prefer capability resolution. Reserve excludes for true removal.

open as a page

What is the purpose of `because("reason")` on a dependency or exclude, and where does that reason surface?

level: juniorimportance: nice to knowfreq 25%

basics

~10 s

because(...) attaches a human-readable justification to a dependency or constraint. It documents why the choice was made and shows up in dependency-insight reports, helping future maintainers understand the graph.

open as a page