Dependency Management
Effective dependency management maintains secure, stable, and maintainable software. Dependencies provide tested functionality but create supply chain vulnerabilities, version conflicts, and maintenance overhead. This guide provides comprehensive strategies for selecting, versioning, updating, and securing dependencies across your technology stack.
Core Principles
- Be Selective: Every dependency is a commitment to ongoing maintenance and security monitoring
- Stay Current: Regular, incremental updates are safer and easier than large, delayed upgrades
- Secure by Default: Vulnerability scanning and remediation should be automated and continuous
- Understand Your Tree: Know what transitive dependencies you're pulling in
- Lock Predictably: Use lock files to ensure reproducible builds across environments
Dependency Selection Criteria
Before adding any new dependency to your project, evaluate it against these criteria. A poor choice can lead to security vulnerabilities, abandonment issues, or license conflicts.
Maturity and Stability
A mature library has proven itself through production use and has established patterns for handling edge cases. Look for:
- Version History: Libraries at v1.0+ indicate API stability; pre-1.0 versions may have breaking changes frequently
- Production Usage: Check download counts (npm, Maven Central) and usage in well-known projects (GitHub dependency graph)
- Breaking Change Frequency: Review release notes to understand how often breaking changes occur
A library with millions of downloads and several years of history is generally safer than a new library with similar functionality. However, maturity alone isn't sufficient - an old library might be unmaintained.
Active Maintenance
Unmaintained dependencies are security risks. An abandoned library won't receive patches for newly discovered vulnerabilities. Evaluate maintenance by:
- Recent Commits: Check the repository for activity in the last 3-6 months
- Issue Response Time: Review how quickly maintainers respond to issues and security reports
- Regular Releases: Look for a pattern of regular releases, not just sporadic activity
- Maintainer Count: Projects with multiple active maintainers are more resilient to abandonment
# Check last commit date for an npm package
npm view <package-name> time
# View repository activity
git log --since="6 months ago" --oneline | wc -l
If a library is unmaintained but critical to your project, consider forking and maintaining it yourself, or investigate actively maintained alternatives. Document this decision in an Architecture Decision Record (ADR).
Security Record
A library's security history reveals how seriously maintainers take security and how quickly they respond to vulnerabilities:
- Known Vulnerabilities: Search the National Vulnerability Database (NVD) or Snyk Vulnerability DB
- Security Policy: Check for a SECURITY.md file indicating how to report vulnerabilities
- Past Response Times: How quickly were previous CVEs patched?
- Disclosure Practices: Do maintainers follow responsible disclosure?
# Check for known vulnerabilities in npm dependencies
npm audit
# Check Java dependencies (using OWASP Dependency-Check)
./gradlew dependencyCheckAnalyze
Some ecosystems have security working groups that proactively audit popular packages (e.g., npm's Security Working Group, OpenSSF). Prefer packages that have undergone security audits.
License Compatibility
License violations can create legal risks for your organization. Common licenses fall into categories:
- Permissive: MIT, Apache 2.0, BSD - allow commercial use with minimal restrictions
- Copyleft: GPL, LGPL - require derivative works to use the same license (potential issues for proprietary software)
- Proprietary: May require commercial licenses or have usage restrictions
Most organizations maintain an approved license list. Common approved licenses include MIT, Apache 2.0, BSD-3-Clause, and ISC. Consult your legal team for specific guidance.
# Generate license report for npm project
npx license-checker --summary
# Gradle license report plugin
./gradlew generateLicenseReport
Verify that transitive dependencies use compatible licenses. A permissively-licensed library might depend on GPL-licensed code, creating unexpected licensing obligations.
Community and Ecosystem
Strong community support indicates a library will remain relevant and well-integrated:
- Documentation Quality: Comprehensive docs with examples, migration guides, and API references
- Community Size: Active Stack Overflow questions, Discord/Slack communities, GitHub discussions
- Framework Integration: Official integrations or plugins for your framework (e.g., Spring Boot starters)
- Alternative Options: Availability of competing solutions suggests ecosystem maturity
A library that's well-integrated into your framework's ecosystem (like an official Spring Boot starter) will be easier to configure and maintain than one requiring custom integration code.
Semantic Versioning and Version Pinning
Semantic Versioning (SemVer) uses a MAJOR.MINOR.PATCH format to communicate the nature of changes:
- MAJOR (1.x.x -> 2.x.x): Breaking changes that require code modifications
- MINOR (1.1.x -> 1.2.x): New features added in a backward-compatible manner
- PATCH (1.1.1 -> 1.1.2): Backward-compatible bug fixes
Understanding SemVer is critical for choosing appropriate version constraints in your dependency declarations.
Version Range Operators
Different package managers use different syntaxes for version ranges:
npm/Yarn (package.json):
^1.2.3(caret): Allow MINOR and PATCH updates (1.2.3 -> 1.9.9, but not 2.0.0)~1.2.3(tilde): Allow only PATCH updates (1.2.3 -> 1.2.9, but not 1.3.0)1.2.3(exact): Only this specific version*orlatest: Any version (avoid in production)
Maven/Gradle:
[1.2.3]: Exact version only1.2.3: Prefer this version, but allow transitive dependency resolution to use others[1.0.0,2.0.0): Range from 1.0.0 (inclusive) to 2.0.0 (exclusive)1.+: Latest version in the 1.x range
// package.json - npm example
{
"dependencies": {
"react": "^19.0.0", // Allow 19.0.0 through 19.x.x
"lodash": "~4.17.21", // Allow 4.17.21 through 4.17.x
"express": "4.21.2" // Exact version (with lock file)
}
}
// build.gradle - Gradle example
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:3.5.0' // Exact
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.+' // Latest 2.15.x
testImplementation 'org.junit.jupiter:junit-jupiter:5.12.0'
}
Lock Files for Reproducibility
Lock files ensure that every developer and every CI/CD environment installs exactly the same versions, preventing "works on my machine" issues:
- npm/Yarn/pnpm:
package-lock.json/yarn.lock/pnpm-lock.yaml - Maven: No built-in lock file; use Maven Lock File Plugin or container-based builds
- Gradle:
gradle.lock(enable with dependency locking)
# Generate lock file (npm)
npm install
# Update lock file after package.json changes
npm install
# Gradle dependency locking
./gradlew dependencies --write-locks
Always commit lock files to version control. They are essential for reproducible builds and security audits. The lock file captures the complete dependency tree with exact versions, including all transitive dependencies.
When to Use Each Strategy
| Scenario | Recommended Strategy | Rationale |
|---|---|---|
| Production application | Caret (^) or tilde (~) with lock file | Balance between receiving patches and stability |
| Library/SDK | Widest compatible range | Allow consumers flexibility in dependency resolution |
| Security-critical dependency | Exact version, manual updates | Full control over vulnerability patching |
| Internal monorepo | Exact versions across all packages | Ensure consistency across microservices |
| Experimental/POC project | Caret (^) or latest | Faster iteration, less maintenance |
For most production applications, use caret ranges (^) with a committed lock file. This allows automated tools like Dependabot to propose patch and minor updates while maintaining reproducibility through the lock file.
Dependency Update Strategies
Keeping dependencies current reduces security risk and technical debt, but updates must be managed carefully to avoid breaking production systems.
Automated Dependency Updates
Automated tools create pull requests when new versions are available:
- Dependabot (GitHub): Supports npm, Maven, Gradle, and many others
- Renovate (GitHub, GitLab, Bitbucket): Highly configurable, supports complex monorepo scenarios
- Snyk: Focuses on security updates with vulnerability context
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
groups:
# Group patch updates together
patch-updates:
update-types:
- "patch"
- package-ecosystem: "gradle"
directory: "/"
schedule:
interval: "weekly"
reviewers:
- "backend-team"
Automated PRs should trigger your CI/CD pipeline. Configure your pipeline to run comprehensive test suites - unit, integration, and end-to-end tests - before merging. See our CI Testing Best Practices for guidance.
Manual Quarterly Reviews
While automation handles routine updates, schedule quarterly manual reviews to:
- Evaluate major version upgrades that automation skips
- Review breaking changes in new major versions
- Remove unused dependencies (reduce attack surface)
- Audit license changes
- Assess alternative libraries that may have emerged
# Identify outdated npm packages
npm outdated
# Identify outdated Gradle dependencies
./gradlew dependencyUpdates
During quarterly reviews, create a tracking issue or spike story to investigate major upgrades. Major version changes often require code modifications and thorough testing. Don't rush these updates - plan time for migration work.
Update Prioritization
Not all updates are equal. Prioritize based on:
- Security Patches: Always highest priority; merge ASAP after testing
- Bug Fixes: High priority if the bug affects your use case
- Minor Features: Medium priority; evaluate value vs. risk
- Major Versions: Low priority unless current version is approaching end-of-life
Use semantic versioning signals to assess risk:
- Patch updates (1.2.3 -> 1.2.4): Low risk, high value - merge quickly
- Minor updates (1.2.4 -> 1.3.0): Medium risk, review changelog for new features
- Major updates (1.3.0 -> 2.0.0): High risk, plan migration effort
Testing Update PRs
Before merging any dependency update:
- Review the Changelog: Understand what changed and why
- Run Full Test Suite: Unit, integration, and E2E tests
- Check for Deprecation Warnings: May indicate future breaking changes
- Verify Lock File Changes: Ensure no unexpected transitive updates
- Manual Smoke Testing: Test critical user paths in staging environment
Deploy larger updates to staging before production. Framework updates (e.g., Spring Boot 3.4 -> 3.5) affect many parts of the application and cause production outages when breaking changes aren't caught in staging.
Vulnerability Scanning
Dependency vulnerabilities are a primary attack vector. Automated scanning detects known CVEs in your dependencies and should be integrated into both development workflows and CI/CD pipelines.
Scanning Tools
Different tools offer varying capabilities:
Sonatype Nexus Lifecycle:
- Enterprise-grade dependency intelligence
- Policy enforcement (block builds on critical vulnerabilities)
- License compliance checking
- Deep analysis of transitive dependencies
Snyk:
- Developer-friendly UI with clear remediation guidance
- IDE integration (VS Code, IntelliJ)
- Prioritization based on reachability analysis (is vulnerable code actually used?)
- Automated PRs for vulnerability fixes
Dependabot Security Alerts (GitHub):
- Free and built-in for GitHub repositories
- Automatic PRs for known vulnerabilities
- Limited to GitHub Advisory Database
npm audit / Gradle dependency-check:
- Free, command-line tools
- Good for local development and CI integration
- Limited remediation guidance compared to commercial tools
# npm vulnerability scan
npm audit
# Fix automatically (only safe patches)
npm audit fix
# Gradle OWASP Dependency-Check
./gradlew dependencyCheckAnalyze
# Snyk CLI scan
snyk test
CI/CD Integration
Integrate vulnerability scanning directly into your CI/CD pipeline to prevent vulnerable code from reaching production:
# .gitlab-ci.yml example
dependency_scan:
stage: test
script:
- npm audit --audit-level=high
- npx snyk test --severity-threshold=high
allow_failure: false # Block merge if vulnerabilities found
Configure pipelines to fail on vulnerabilities above a certain severity threshold. Common thresholds:
- Block on: Critical and High severity
- Warn on: Medium severity
- Report only: Low severity
See our Pipeline Configuration Guide for more examples.
Vulnerability Remediation Workflow
When a vulnerability is detected:
-
Assess Severity and Exploitability:
- Is the vulnerable code actually executed in your application?
- What's the CVSS score and attack vector?
- Is there active exploitation in the wild?
-
Check for Patches:
- Is a patched version available?
- Does the patch introduce breaking changes?
-
Remediation Options:
- Upgrade: Best option if a patched version exists
- Workaround: If no patch, can you avoid using the vulnerable functionality?
- Replace: Switch to an alternative library if the dependency is unmaintained
- Accept Risk: Document decision if risk is low and remediation is not feasible (requires security team approval)
-
Test Thoroughly:
- Verify the vulnerability is resolved
- Ensure no regressions were introduced
-
Document:
- Record the CVE, remediation steps, and decision rationale
- Update security runbooks if patterns emerge
False Positives and Suppression
Not all reported vulnerabilities affect your application. For example:
- Vulnerabilities in development-only dependencies
- Code paths that are never executed in your context
- Vulnerabilities that require specific configurations you don't use
When suppressing a vulnerability, document the rationale:
<!-- OWASP Dependency-Check suppression file -->
<suppressions>
<suppress>
<cve>CVE-2023-12345</cve>
<gav>com.example:vulnerable-lib:1.0.0</gav>
<notes>
Vulnerability is in LDAP functionality which we do not use.
Tracked in JIRA-1234. Review on next major upgrade.
</notes>
</suppress>
</suppressions>
Suppressions should be:
- Justified: Clear explanation of why it's safe to suppress
- Time-limited: Re-evaluate during quarterly reviews
- Tracked: Link to a tracking issue for long-term resolution
Transitive Dependency Management
Transitive dependencies are dependencies of your dependencies. A typical project might have dozens of direct dependencies but hundreds of transitive ones. These indirect dependencies can introduce vulnerabilities, version conflicts, and bloat.
Understanding Dependency Trees
Visualize your full dependency tree to understand what's being pulled in:
# npm dependency tree
npm list
# Show only production dependencies, limit depth
npm list --production --depth=2
# Gradle dependency tree
./gradlew dependencies
# Show only runtime dependencies
./gradlew dependencies --configuration runtimeClasspath
This output reveals:
- Which transitive dependencies are included
- Version conflicts
- Duplicate dependencies at different versions
- Unexpectedly large dependency trees
Version Conflicts
Version conflicts occur when different parts of your dependency tree require different versions of the same library:
|-- [email protected]
| `-- [email protected]
`-- [email protected]
`-- [email protected]
Most package managers resolve this automatically, but the resolution strategy differs:
npm/Yarn: Uses a flat dependency tree when possible; may duplicate dependencies if versions are incompatible Maven/Gradle: Uses "nearest wins" or "newest wins" strategies
Manual intervention is sometimes necessary:
// Gradle: Force a specific version
configurations.all {
resolutionStrategy {
force 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
}
}
// npm: Use resolutions (Yarn) or overrides (npm 8.3+)
{
"overrides": {
"transitive-dep": "1.2.3"
}
}
Be cautious when forcing versions - you might introduce incompatibilities. Always test thoroughly after forcing version resolution.
Dependency Exclusions
Sometimes you need to exclude a transitive dependency entirely, often because:
- It conflicts with another version you're using
- It's not needed for your use case
- It introduces a security vulnerability
// Gradle: Exclude a transitive dependency
dependencies {
implementation('com.example:library:1.0.0') {
exclude group: 'org.unwanted', module: 'unwanted-dep'
}
}
<!-- Maven: Exclude transitive dependency -->
<dependency>
<groupId>com.example</groupId>
<artifactId>library</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>org.unwanted</groupId>
<artifactId>unwanted-dep</artifactId>
</exclusion>
</exclusions>
</dependency>
Document why you're excluding dependencies - future maintainers need to understand the reasoning.
Managing Transitive Security Vulnerabilities
When a vulnerability is in a transitive dependency:
-
Check if a Newer Version Fixes It:
# See which package brings in the vulnerable dependency
npm ls vulnerable-package
# Update the parent dependency to a version that uses a patched version
npm update parent-package -
Override the Version: If updating the parent doesn't work, override the vulnerable transitive dependency:
{
"overrides": {
"vulnerable-package": "1.2.3"
}
} -
Contact the Maintainer: Open an issue on the parent package's repository requesting an update to the vulnerable dependency
Dependency Mediation Strategies
For large projects or monorepos, centralize dependency version management:
Maven Bill of Materials (BOM):
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Gradle Version Catalog:
# gradle/libs.versions.toml
[versions]
spring-boot = "3.2.1"
jackson = "2.15.2"
[libraries]
spring-boot-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" }
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
Centralized version management ensures consistency across modules and simplifies updates.
Monorepo Dependency Management
Monorepos containing multiple packages or microservices introduce additional challenges:
Workspace Dependencies
Use workspace features to share code between packages without publishing:
// npm workspaces (package.json at root)
{
"workspaces": [
"packages/*",
"services/*"
]
}
// Gradle multi-project build (settings.gradle)
include 'shared-lib', 'service-a', 'service-b'
Workspace dependencies are symlinked locally, allowing real-time development across packages without publishing.
Version Consistency
Ensure all packages use the same versions of shared dependencies to avoid runtime issues:
Lerna (npm monorepo tool):
# Ensure all packages use the same version of React
npx lerna exec -- npm install [email protected]
Gradle Version Catalog (as shown above): Define versions once, reference everywhere
Dependency Deduplication
Monorepos can accumulate duplicate dependencies across packages. Periodically deduplicate:
# npm: Deduplicate dependencies
npm dedupe
# Yarn: Use yarn dedupe plugin
Independent vs. Fixed Versioning
Choose a versioning strategy for your monorepo:
- Independent: Each package has its own version (common for libraries)
- Fixed/Locked: All packages share the same version (common for applications)
Independent versioning provides flexibility but complicates dependency management. Fixed versioning simplifies releases but may lead to unnecessary version bumps.
Best Practices Summary
| Practice | Rationale | Tooling |
|---|---|---|
| Evaluate before adding | Reduce supply chain risk | Manual review checklist |
| Commit lock files | Ensure reproducible builds | package-lock.json, yarn.lock, pnpm-lock.yaml, Gradle locks |
| Use caret ranges with locks | Balance updates and stability | ^ in package.json, lock file committed |
| Automate security scanning | Catch vulnerabilities early | Dependabot, Snyk, npm audit, OWASP Dependency-Check |
| Regular updates | Reduce technical debt | Dependabot, Renovate, quarterly reviews |
| Test updates thoroughly | Prevent regressions | CI/CD test suite, staging deployments |
| Document overrides and exclusions | Future maintainability | Inline comments, ADRs |
| Centralize versions in monorepos | Consistency across services | Gradle version catalogs, Lerna |
| Monitor transitive dependencies | Prevent hidden vulnerabilities | npm ls, gradle dependencies, dependency tree analysis |
Related Guidelines
- Security Best Practices - Broader security context for dependency management
- Code Review Guidelines - Reviewing dependency changes in PRs
- Pipeline Configuration - Integrating dependency scanning into CI/CD
- Technical Debt Management - Handling outdated dependencies as technical debt
- Docker Best Practices - Dependency management in containerized builds