Skip to main content

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

  1. Be Selective: Every dependency is a commitment to ongoing maintenance and security monitoring
  2. Stay Current: Regular, incremental updates are safer and easier than large, delayed upgrades
  3. Secure by Default: Vulnerability scanning and remediation should be automated and continuous
  4. Understand Your Tree: Know what transitive dependencies you're pulling in
  5. 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
  • * or latest: Any version (avoid in production)

Maven/Gradle:

  • [1.2.3]: Exact version only
  • 1.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

ScenarioRecommended StrategyRationale
Production applicationCaret (^) or tilde (~) with lock fileBalance between receiving patches and stability
Library/SDKWidest compatible rangeAllow consumers flexibility in dependency resolution
Security-critical dependencyExact version, manual updatesFull control over vulnerability patching
Internal monorepoExact versions across all packagesEnsure consistency across microservices
Experimental/POC projectCaret (^) or latestFaster 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:

  1. Security Patches: Always highest priority; merge ASAP after testing
  2. Bug Fixes: High priority if the bug affects your use case
  3. Minor Features: Medium priority; evaluate value vs. risk
  4. 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:

  1. Review the Changelog: Understand what changed and why
  2. Run Full Test Suite: Unit, integration, and E2E tests
  3. Check for Deprecation Warnings: May indicate future breaking changes
  4. Verify Lock File Changes: Ensure no unexpected transitive updates
  5. 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:

  1. 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?
  2. Check for Patches:

    • Is a patched version available?
    • Does the patch introduce breaking changes?
  3. 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)
  4. Test Thoroughly:

    • Verify the vulnerability is resolved
    • Ensure no regressions were introduced
  5. 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:

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:

  1. 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
  2. Override the Version: If updating the parent doesn't work, override the vulnerable transitive dependency:

    {
    "overrides": {
    "vulnerable-package": "1.2.3"
    }
    }
  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

PracticeRationaleTooling
Evaluate before addingReduce supply chain riskManual review checklist
Commit lock filesEnsure reproducible buildspackage-lock.json, yarn.lock, pnpm-lock.yaml, Gradle locks
Use caret ranges with locksBalance updates and stability^ in package.json, lock file committed
Automate security scanningCatch vulnerabilities earlyDependabot, Snyk, npm audit, OWASP Dependency-Check
Regular updatesReduce technical debtDependabot, Renovate, quarterly reviews
Test updates thoroughlyPrevent regressionsCI/CD test suite, staging deployments
Document overrides and exclusionsFuture maintainabilityInline comments, ADRs
Centralize versions in monoreposConsistency across servicesGradle version catalogs, Lerna
Monitor transitive dependenciesPrevent hidden vulnerabilitiesnpm ls, gradle dependencies, dependency tree analysis

Further Reading