Skip to main content
Gradle Dependency Triage

Dependency Conflict? Here's Your Opolis Step-by-Step Guide to Gradle Triage (With a Handy Triage Card)

You're mid-sprint, the build was green yesterday, and now Gradle spits out a wall of red: Conflict dependency . The error message mentions two versions of the same library, but the stack trace points to a class you've never heard of. Sound familiar? Dependency conflicts are one of the most common and frustrating issues in Gradle builds, especially as projects grow and pull in more transitive dependencies. This guide gives you a repeatable triage process—a step-by-step method to identify, isolate, and resolve conflicts without guesswork. We'll also share a handy triage card you can print and keep at your desk. Why Dependency Conflicts Happen (and Why They're So Tricky) Gradle's dependency resolution is powerful but can be opaque. When you declare a dependency in your build.gradle file, Gradle pulls in not just that library but all of its dependencies (transitive dependencies).

You're mid-sprint, the build was green yesterday, and now Gradle spits out a wall of red: Conflict dependency. The error message mentions two versions of the same library, but the stack trace points to a class you've never heard of. Sound familiar? Dependency conflicts are one of the most common and frustrating issues in Gradle builds, especially as projects grow and pull in more transitive dependencies. This guide gives you a repeatable triage process—a step-by-step method to identify, isolate, and resolve conflicts without guesswork. We'll also share a handy triage card you can print and keep at your desk.

Why Dependency Conflicts Happen (and Why They're So Tricky)

Gradle's dependency resolution is powerful but can be opaque. When you declare a dependency in your build.gradle file, Gradle pulls in not just that library but all of its dependencies (transitive dependencies). If two libraries depend on different versions of the same third-party library, Gradle must choose one version to use. By default, Gradle picks the newest version, but that can break things if the newer version has breaking API changes or if the older version is required for compatibility.

For example, suppose your project uses Library A (which depends on Library X v2.0) and Library B (which depends on Library X v1.5). Gradle resolves to v2.0, but Library B might have been compiled against v1.5 and expects a class or method removed in v2.0. The result: a NoSuchMethodError or ClassNotFoundException at runtime. The error message might only mention the class name, not the conflicting versions, making it hard to trace back to the root cause.

Another common scenario is upgrading a direct dependency—its transitive dependencies change. A seemingly minor upgrade can introduce a new version of a library that clashes with another dependency you already have. This is especially common in large projects with many modules, where each module may have its own version of a dependency.

The key insight: dependency conflicts aren't just about version numbers—they're about classpath order and API compatibility. Gradle's resolution strategy can be configured, but without a systematic approach, you can end up in a cycle of trial and error. That's where triage comes in.

Your Opolis Triage Framework: The 4-Step Process

We've developed a simple four-step framework that works for any Gradle project. Think of it as a triage card: each step narrows down the problem until you have a clear fix.

Step 1: Capture the conflict output. Run ./gradlew dependencies --configuration compileClasspath (or runtimeClasspath) and pipe the output to a file. Look for lines with -> arrows—they indicate version conflicts and the resolved version.

Step 2: Identify the conflicting libraries. Search for the library name mentioned in the error. Use ./gradlew dependencyInsight --dependency <library> --configuration compileClasspath to see why each version is requested and which dependency pulls it in.

Step 3: Determine the resolution strategy. Decide whether to force a version, exclude a transitive dependency, or upgrade/downgrade a direct dependency. Use force only as a last resort—it can mask deeper issues.

Step 4: Test and verify. After applying a fix, run the full test suite and check for runtime errors. If the conflict reappears, revisit step 2—there may be another path pulling in the wrong version.

This process works for both compile-time and runtime conflicts. Always start with the dependency tree output, not the error message alone. The error tells you what broke; the tree tells you why.

How Gradle Resolves Conflicts Under the Hood

To triage effectively, you need to understand Gradle's resolution algorithm. When Gradle encounters multiple versions of the same module, it uses a strategy called conflict resolution. By default, it selects the newest version (the highest version number). But that's not always safe—newer versions can introduce breaking changes, and sometimes the older version is the one that works.

Gradle also considers dependency constraints and forced versions. A dependency constraint is a way to specify a preferred version without forcing it. For example, you can declare a constraint like "I prefer version 2.0, but if another dependency requires 1.5, it's okay to use 1.5." That's different from a forced version, which overrides everything and can cause conflicts if not used carefully.

Another important concept is transitive dependency management. When you add a dependency, Gradle automatically pulls in its transitive dependencies. You can exclude specific transitive dependencies using the exclude keyword, but this can be brittle—if the library's dependencies change in a future version, your exclusion may no longer work.

Gradle also supports version locking and dependency locking for reproducible builds. With dependency locking, you pin all transitive versions to a known-good state. This is especially useful in CI/CD pipelines where you want to avoid surprises. However, locking can make upgrades harder, as you need to manually update the lock file.

Understanding these mechanisms helps you choose the right fix. For instance, if a transitive dependency is causing a conflict, you might prefer to add a direct dependency with a specific version rather than forcing or excluding. That gives you more control and makes the dependency explicit.

Worked Example: Resolving a Real-World Conflict

Let's walk through a typical scenario. You're working on a Spring Boot application that uses com.google.guava:guava. Your project directly depends on Guava 30.1-jre, but one of your dependencies—say, my-custom-lib—depends on Guava 23.0. Gradle resolves to 30.1-jre (the newest), but your custom library was compiled against 23.0 and uses a class removed in 30.1. You get a NoSuchMethodError at runtime.

Here's how you'd triage it:

  1. Run ./gradlew dependencies --configuration runtimeClasspath | grep guava. You see two entries: one from your project (30.1-jre) and one from my-custom-lib (23.0). The output shows -> 30.1-jre, meaning Gradle selected the newer version.
  2. Run ./gradlew dependencyInsight --dependency guava --configuration runtimeClasspath. This shows that my-custom-lib requests 23.0, and your project requests 30.1-jre. The insight also shows the dependency path: my-custom-lib -> guava:23.0.
  3. You have several options:
    • Option A: Upgrade my-custom-lib to a version that supports Guava 30.1-jre. Check if a newer version exists.
    • Option B: Downgrade your direct Guava dependency to 23.0. This might break other parts of your code that use newer APIs.
    • Option C: Use a dependency constraint to force 30.1-jre, but then you risk breaking my-custom-lib. You'd need to verify that the removed class is not used at runtime.
    • Option D: Exclude the transitive Guava from my-custom-lib and rely on your own version. This is risky because the library might depend on Guava internally.
  4. After testing, you find that my-custom-lib actually works fine with Guava 30.1-jre—the removed class is only used in a rarely called code path. So you add a constraint: constraints { implementation('com.google.guava:guava:30.1-jre') { because 'my-custom-lib works with this version' } }. This tells Gradle to prefer 30.1-jre but allows fallback if needed.

This example shows that the right fix often involves understanding the actual runtime behavior, not just the version numbers.

Edge Cases and Exceptions

Not all conflicts are straightforward. Here are some edge cases you might encounter:

Conflict with the same library but different classifiers

Sometimes a library is published with multiple classifiers (e.g., jdk8, jdk11, android). Gradle treats these as different modules, but they can still cause classpath issues if they share the same package names. For example, com.example:lib:1.0:jdk8 and com.example:lib:1.0:jdk11 might both end up on the classpath, leading to duplicate classes. The fix is to ensure you only include the classifier you need.

Version range requirements

Some dependencies declare version ranges (e.g., [1.0,2.0)). Gradle resolves these by selecting the highest version within the range. If two ranges overlap, the resolution can be complex. For instance, if Library A requires [1.0,1.5] and Library B requires [1.4,2.0), Gradle picks the highest common version, which might be 1.5. This is usually fine, but if the ranges are incompatible, you'll get a resolution error. In that case, you may need to exclude one of the libraries or use a forced version.

Forced versions and platform dependencies

When using a BOM (Bill of Materials) or a platform like Spring Boot's dependency management, versions are often forced. That can override your explicit versions and cause conflicts. For example, if you set spring-boot-starter-parent as a parent, it may force a specific version of a library that clashes with another dependency. The fix: either align your dependencies with the platform or override the platform's version using a property (if supported).

Another edge case: a conflict involving a library that is a transitive dependency of multiple modules in a multi-module project. The conflict might appear in one module but not another, depending on the dependency graph. You need to check each module's configuration separately.

Limits of the Triage Approach (and When to Call in Reinforcements)

Our four-step triage process works for most conflicts, but it has limits. Here's what it can't do:

  • Fix binary incompatibility: If a library's API changes between versions, no amount of version forcing will make it work. You need to update your code or find an alternative library.
  • Handle missing dependencies: If a library is not in any repository, Gradle will fail to resolve it. The triage process assumes all dependencies are available.
  • Resolve conflicts in custom configurations: If you have custom configurations with complex resolution strategies, the standard dependencies output might not show the full picture. You may need to inspect the configuration's resolution result programmatically.

When should you seek a deeper solution? If you find yourself repeatedly fixing the same conflict, it's a sign that your dependency management needs an overhaul. Consider using a dependency lock file, adopting a BOM, or migrating to a more modular architecture. Also, if the conflict involves a security vulnerability (e.g., a library with a CVE), you should prioritize upgrading even if it causes temporary breakage.

Another limitation: the triage process can be time-consuming for large projects with hundreds of dependencies. In those cases, you might want to use tools like Gradle's build scans or third-party plugins that visualize the dependency graph. These can help you spot patterns faster.

Reader FAQ

How do I find out which dependency is pulling in a conflicting transitive library?

Use the dependencyInsight task. For example: ./gradlew dependencyInsight --dependency com.google.guava --configuration compileClasspath. This shows the dependency tree for that specific module, including all paths that request it.

What's the difference between force and constraints?

force overrides any other version, regardless of what other dependencies require. It's a blunt instrument. constraints set a preferred version but still allow Gradle to select a different version if another dependency requires it. Use constraints when you want to influence resolution without breaking other dependencies.

Should I always use the newest version to avoid conflicts?

Not necessarily. Newer versions can introduce breaking changes or require different transitive dependencies. Always test after upgrading. In some cases, staying on an older version that is well-tested is safer.

How can I prevent conflicts from happening in the first place?

Regularly review your dependency tree, use dependency locking for reproducible builds, and keep your dependencies up to date. Also, consider using a BOM to align versions across your project. Finally, run ./gradlew dependencies as part of your CI pipeline to catch conflicts early.

What if the conflict only appears at runtime, not during compilation?

This often happens when a class is loaded lazily or only in certain code paths. Use the runtimeClasspath configuration to check runtime dependencies. You can also use java -verbose:class to see which classes are loaded from which JARs.

Can I exclude a transitive dependency globally?

Yes, you can use configurations.all { exclude group: 'com.example', module: 'unwanted' }. But this can cause unexpected failures if the excluded library is needed. Prefer excluding at the dependency level.

How do I handle conflicts in a multi-module project?

Check each module's dependency tree separately. You can also define a common version in the root project using ext or a BOM. Use allprojects or subprojects to apply constraints consistently.

Now that you have a systematic process, the next time you see a dependency conflict, you won't panic. Print out the four-step triage card, follow the steps, and you'll resolve it in minutes instead of hours. And remember: the dependency tree is your friend—learn to read it, and you'll master Gradle dependency management.

Share this article:

Comments (0)

No comments yet. Be the first to comment!