Excludes are often a code smell. When are they justified, and what modern Gradle mechanisms should you prefer for managing the transitive graph?
answer
- exclude = true removal only
- version conflict -> constraint/strictly
- family alignment -> platform/BOM
- same capability -> capabilitiesResolution
- centralize in convention plugin, use because()
basics
~10 sExcludes 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.
solid answer
~50 sExcludes delete nodes from the graph, which is the right move only when an artifact should simply not be there — e.g. a second logging backend, an annotation jar you don't need, or a library with over-declared metadata. They become a smell when used to dodge problems better solved declaratively. If two libraries demand incompatible versions, that's a **constraint** or a **platform/BOM** alignment job, not an exclude. If two artifacts provide the same *capability* (e.g. `org.slf4j:slf4j-simple` vs `ch.qos.logback:logback-classic` both being SLF4J backends, or Guava's `-jre` vs `-android` variants), the modern, intentional tool is **capability conflict resolution** via `resolutionStrategy.capabilitiesResolution`, which makes the choice explicit and fails fast on ambiguity. Whatever you choose, attach `because()` and prefer centralizing rules in a platform or convention plugin so the policy is one auditable place rather than scattered excludes that silently rot.
code
kotlin · 9 linesconfigurations.all {
resolutionStrategy {
// two artifacts claim the same capability -> choose intentionally
capabilitiesResolution.withCapability("com.google.collections:google-collections") {
select("com.google.guava:guava:0")
because("prefer Guava over the legacy google-collections capability")
}
}
}go deeper
Just know excludes remove unwanted artifacts; deeper strategy is not expected.
Distinguish 'remove an artifact' (exclude) from 'pick a version' (constraint) and name platforms for alignment.
Choose the right mechanism per problem class, use capability resolution for competing implementations, and document with because().
Own dependency governance: centralize constraints/platforms/capability rules in a convention plugin, minimize ad-hoc excludes, and make the policy auditable and enforced across teams.
## Excludes as a last resort An `exclude` removes a coordinate from the graph. That's appropriate when the artifact genuinely shouldn't exist in your build: - **Duplicate facilities** — two logging backends, two JSON libs doing the same job. - **Broken/over-declared metadata** — the POM lists something the code never uses. - **Truly unwanted transitives** — an optional feature you don't enable. It's a *smell* when used to paper over a problem with a cleaner declarative answer. ## Prefer: constraints (version problems) When the issue is *which version*, use a **dependency constraint**. Constraints influence selected versions without adding a dependency, and support `require`/`prefer`/`strictly`/`reject`: ```kotlin dependencies { constraints { implementation("com.fasterxml.jackson.core:jackson-databind") { version { strictly("2.17.1") } because("align all Jackson modules + CVE fix") } } } ``` ## Prefer: platforms / BOMs (alignment) A **platform** (`platform(...)`/`enforcedPlatform(...)`) imports a set of aligned versions so families like Jackson or Spring move together. This solves whole classes of conflicts that people otherwise patch with excludes. ## Prefer: capability resolution (competing implementations) Gradle Module Metadata can declare **capabilities** — "I provide capability X". When two artifacts provide the *same* capability, Gradle reports a conflict instead of silently putting both on the classpath. You resolve it intentionally: ```kotlin configurations.all { resolutionStrategy.capabilitiesResolution.withCapability("org.slf4j:slf4j-impl") { selectHighestVersion() } } ``` This is the modern replacement for the old "exclude one of the two backends" hack — the intent ("these are mutually exclusive, pick one") is explicit and ambiguity fails the build. ## Strategy summary | Problem | Tool | |---|---| | Wrong/conflicting version | constraint (`strictly`/`reject`) | | Whole family must align | platform/BOM | | Two artifacts = same capability | capabilitiesResolution | | Artifact genuinely unwanted | exclude / isTransitive=false | Centralize these in a **platform project** or **convention plugin** so policy lives in one auditable place, and annotate every non-obvious decision with `because()`. Excludes scattered across modules are exactly what makes large builds inscrutable.
- Why is capability resolution preferable to excluding one of two competing backends?It makes the mutual exclusion explicit and fails the build on ambiguity, so a new conflicting artifact can't slip onto the classpath unnoticed. An exclude is a one-off deletion that doesn't guard against future duplicates and gives no diagnostic when the situation changes.
- How does a constraint differ from a force/exclude for fixing a version?A constraint declares a version rule (require/strictly/reject) without adding the dependency, participates in conflict resolution, and is reported with its reason. Excludes can't set versions, and `resolutionStrategy.force` is a blunter, legacy hammer that strictly overrides without negotiation.
saying these in an interview costs you the question
- Reaching for excludes to resolve version conflicts that constraints/platforms handle cleanly.
- Scattering excludes across modules with no documented reason, leaving the graph unauditable.
- Not knowing capability resolution exists for competing-implementation conflicts.