Dependency Hell — a situation where the package manager cannot resolve version conflicts of libraries in a project. In mobile development, Dependency Hell is especially painful: Gradle on Android and CocoaPods/SPM on iOS often face transitive conflicts. According to a Sonatype (2024) report, the average number of direct dependencies in a mobile project exceeds 80, and transitive ones — 400+, each requiring version compatibility.
Key Takeaways
Dependency Hell is a term describing a situation where the dependency management system cannot resolve a version conflict between libraries. The project requires library A version 1.x and library B version 2.x, but A depends on C version 1.0, while B depends on C version 2.0, and C:1.0 and C:2.0 are incompatible.
The problem is common across all ecosystems with package managers. In Android — Gradle conflicts between support library and AndroidX. In iOS — CocoaPods conflicts between different versions of Alamofire. In Node.js — npm peer dependency conflicts. In Python — pip resolution failures.
Modern dependency managers (npm v7+, Gradle 7+, SwiftPM) have improved resolution algorithms, but complete elimination of conflicts is impossible with hundreds of transitive dependencies. Dependency Hell has moved from the “build error” category to the “risk management” category.
Diamond dependency — the classic case. Library A depends on D:1.0, library B depends on D:2.0. If A and B are used together, the package manager must decide which version of D to install. In most cases, the maximum version (2.0) is selected, but if A is not compatible with D:2.0 — the conflict is unresolvable.
Version conflict — an explicit mismatch of requirements. A requires Logging >=2.0, B requires Logging <2.0. The manager cannot satisfy both conditions. Peer dependency conflict — plugin A requires React 17, but the project uses React 18 with breaking changes. npm outputs a warning, but the installation proceeds — behavior becomes unpredictable.
Transitive dependency hell — when a dependency is not direct but indirect. The developer doesn’t know that library A depends on B, and B depends on C. Gradle Dependency Tree — a tool for visualizing the entire dependency chain, showing where the conflicting library comes from.
Circular dependency — A depends on B, and B depends on A. Modern managers (Gradle, npm) block circular dependencies at build time. Solution — extract a common module C that both A and B depend on, breaking the cycle.
Growing number of libraries — the main prerequisite. Each module adds direct and transitive dependencies. In an Android project with Jetpack Compose, Firebase, Retrofit, and Coil, the number of transitive dependencies easily exceeds 500. Each new library is a potential conflict.
Unsynchronized updates — teams update libraries at different times. Backend updates Jackson to 2.15, Analytics team uses 2.12. When integrating modules, a conflict arises. Solution — centralized versions (Bill of Materials) in a Gradle BOM file or version catalog.
Different versions of the same library — the classic situation: module A uses OkHttp 3.12, module B uses OkHttp 4.0. If upgrading to 4.0 breaks module A, the project gets stuck on two versions, which can lead to classpath conflicts in Java or duplicate symbols in iOS.
Gradle Dependency Tree — the `gradle dependencies` command outputs the full dependency tree with conflicts indicated. The resolved version shows which version Gradle selected, and conflicting versions are marked with arrows. Example: `com.squareup.okhttp3:okhttp -> 4.9.3 (*)` — version resolved, (*) — duplication.
npm ls — a similar command for Node.js. The `--all` flag shows the full tree. Peer dependency conflicts are output with warnings. SwiftPM Graph — `swift package show-dependencies` shows the dependency graph for iOS projects, including branches and revisions.
Dependency Analysis Plugin — a Gradle plugin from Autonomy that finds unused dependencies and conflicts. Ben Manes Versions Plugin — checks which dependencies are outdated and shows available updates. Both tools automate routine compatibility checks.
// Conflict: module A needs okhttp 3.x, module B needs okhttp 4.x
dependencies {
implementation("com.example:module-a:1.0") // -> okhttp 3.12
implementation("com.example:module-b:2.0") // -> okhttp 4.0
}
// Solution: force a specific version
configurations.all {
resolutionStrategy {
force "com.squareup.okhttp3:okhttp:4.9.3"
}
}
Version Catalog (Gradle 7+) — centralized version declaration in a TOML file. All modules use the same library versions. Example: the `libs.versions.toml` file contains `okhttp = “4.9.3”`, and all modules reference this catalog. Version conflicts between modules are eliminated.
Bill of Materials (Spring BOM) — a Maven concept where compatible library versions are specified. The Google Android team uses Compose BOM for Jetpack libraries. By using a BOM, you get a guarantee that all Compose versions are compatible with each other.
Renovate and Dependabot — automated PR creators for dependency updates. Renovate groups compatible updates, checks for breaking changes via Docker images. Dependabot is a built-in GitHub solution that updates dependencies and checks compatibility through CI.
Semantic Versioning — use caret `^1.2.3` for patch/minor updates and tilde `~1.2.3` for patch-only updates. But even semver does not guarantee compatibility — real semver violations occur in 15% of cases (according to a University of Luxembourg study, 2024). Lock files pin the exact version that passed tests.
Minimize dependencies — every library must be justified. If you can implement the functionality in 20 lines of your own code — don’t add a library. Example: instead of a date formatting library (4 transitive dependencies), use built-in platform tools. The “dependency budget” rule — no more than 50 direct dependencies per project.
Regular updates — update dependencies in small steps, not once a year. Dependabot creates a PR for each update. CI should run the full test suite. DevContainer — a unified development environment where dependency versions match production, eliminating environment conflicts.
Frequently Asked Questions
First, run `gradle dependencies` (Gradle), `npm ls` (Node.js), or `swift package show-dependencies` (SwiftPM). Find the conflicting library. Three solutions: force a version via resolutionStrategy, exclude the transitive dependency (`exclude group:`), or update one of the conflicting libraries to a compatible version.
Version Catalog (libs.versions.toml) — a single source of truth for all library versions. All project modules reference one catalog. When a library is updated, the version changes in one place. This prevents the situation where two modules use different versions of the same library.
Transitive dependencies are libraries pulled in by a direct dependency. The developer is often unaware of them. The danger: a transitive dependency can conflict with another direct dependency. The solution is to regularly check the dependency tree and only include libraries with minimal transitive dependencies.
Not necessarily every sprint, but regularly — yes. Recommendation: once a month, run Dependabot or Renovate to create PRs. Critical security patches should be updated within a week. Minor updates — within a regular sprint. Major updates require a separate assessment of breaking changes.
An unmaintained library is a security and compatibility risk. Strategy: find an alternative with an active community (GitHub stars, last commit date), plan migration through abstraction (Interface/Protocol), replace the library over 2–3 sprints. If there is no alternative — fork the repository and maintain the version within the team.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also