If you’re working on a multi-module Gradle project, encountering circular dependencies can be a frustrating roadblock. You may see an error like this:
Circular dependency between the following tasks:
:javaModule:compileJava
+--- :kotlinModule:compileJava
| +--- :javaModule:compileJava (*)
| \--- :kotlinModule:compileKotlin
| \--- :javaModule:compileJava (*)
\--- :kotlinModule:compileKotlin (*)
This error occurs when two modules depend on each other, creating a loop that prevents Gradle from determining the correct build order.
Note- To recreate this scenario, we created two modules (
javaModule
andkotlinModule
) and intentionally introduced a circular dependency.
In this guide we will cover the root cause of circular dependencies in Gradle and practical solutions to fix them.
Understanding the Problem: What Causes Circular Dependencies in Gradle?
A circular dependency occurs when two or more modules depend on each other, forming a loop that Gradle cannot resolve. In the error above:
javaModule
needskotlinModule
to compile.kotlinModule
depends onjavaModule
to compile its Java code.kotlinModule:compileKotlin
also requiresjavaModule
, reinforcing the cycle.
Common Causes of Circular Dependencies
- Bi-Directional Module Dependencies: If
javaModule
andkotlinModule
both reference each other usingimplementation project(":eachOtherModule")
, Gradle gets stuck in an infinite loop. - Transitive Dependencies: A third-party dependency might be causing an indirect loop between your modules.
- Improper Task Dependencies: Gradle’s build order may be incorrectly configured, forcing modules to compile in a conflicting sequence.
How to Fix Circular Dependencies in Gradle
1. Remove Direct Bi-Directional Dependencies
The most common cause of circular dependencies is when two modules depend on each other directly.
Check your build.gradle
or build.gradle.kts
files:
// javaModule's build.gradle.kts
dependencies {
implementation(project(":kotlinModule")) // Java module depends on Kotlin module
}
// kotlinModule's build.gradle.kts
dependencies {
implementation(project(":javaModule")) // Kotlin module depends on Java module (Creates a cycle)
}
Since both modules reference each other, Gradle cannot determine the correct build order.
Solution: Remove one of the dependencies or refactor the project.
2. Use api
Instead of implementation
for Interface Dependencies
If a module only requires interfaces or abstract classes from another module, use api
instead of implementation
.
// build.gradle.kts
dependencies {
api(project(":javaModule")) // Instead of implementation
// ... other dependencies
}
This allows dependent modules to access the necessary classes without forcing a full compile-time dependency.
3. Introduce a Common Module
If both javaModule
and kotlinModule
share some code, it’s best to move that code into a separate module (e.g., commonModule
).
New Project Structure:
commonModule
├── javaModule (depends on commonModule)
├── kotlinModule (depends on commonModule)
Update build.gradle.kts
files accordingly:
javaModule/build.gradle.kts
// build.gradle.kts
dependencies {
implementation(project(":commonModule"))
// ... other dependencies
}
kotlinModule/build.gradle.kts
// build.gradle.kts
dependencies {
implementation(project(":commonModule"))
// ... other dependencies
}
Now, both javaModule
and kotlinModule
rely on commonModule
, breaking the circular dependency.
4. Use Gradle Dependency Constraints
If you suspect a transitive dependency is causing the issue, use constraints
in dependencies
:
dependencies {
constraints {
implementation(project(":javaModule")) {
because("Avoids circular dependency with kotlinModule")
}
}
}
This helps Gradle prioritize dependencies correctly.
5. Exclude Unnecessary Transitive Dependencies
If kotlinModule
indirectly depends on javaModule
due to a third-party library, exclude it:
dependencies {
implementation(project(":javaModule")) {
exclude(module = "kotlinModule") // Prevents circular dependency
}
}
This prevents Gradle from resolving unnecessary dependencies that might create loops.
6. Adjust Compilation Task Dependencies
By default, Kotlin and Java modules in Gradle may have incorrect build order assumptions. You can explicitly set task dependencies:
tasks.named("compileKotlin") {
dependsOn(tasks.named("compileJava").get())
}
This ensures Kotlin compilation happens after Java compilation, resolving potential conflicts.
Final Checks: Debugging Dependencies
After making changes, run the following command to inspect your project’s dependency tree:
./gradlew dependencies
Lists all dependencies in the project, including:
- Direct dependencies
- Transitive dependencies
- Dependency versions
- Resolved dependency graph
- Helps debug dependency issues, such as conflicts or unexpected transitive dependencies.
If a circular dependency still exists, this command will highlight the problematic modules.
Conclusion
Circular dependencies in Gradle can cause major build issues, but they can be resolved using the right strategies. The best approach depends on the cause:
- If modules depend on each other directly, remove one of the dependencies.
- If shared code is needed, move it to a
commonModule
. - If the issue is caused by transitive dependencies, use
exclude
orconstraints
. - If compilation order is incorrect, manually adjust
tasks.named("compileKotlin")
.
By following these steps, you can eliminate circular dependencies and improve the maintainability of your Gradle projects.