Home/Module System & Architecture/Dependency Management

Dependency Management

Versioning and dependency strategies

intermediate
dependenciescargosemver
🎮 Interactive Playground

What is Dependency Management?

Dependency management in Rust involves selecting, versioning, and maintaining external crates. Good practices ensure security, reproducibility, and minimal bloat.

The Problem

Poor dependency management leads to:

  • Security vulnerabilities: Outdated crates with known issues
  • Version conflicts: Diamond dependency problems
  • Bloat: Unused transitive dependencies
  • Build breakage: Yanked versions, incompatible updates

Example Code

# Cargo.toml - Best practices for dependencies

[package]
name = "myapp"
version = "0.1.0"
edition = "2021"

[dependencies]
# Pin major version - allows compatible updates
tokio = "1"
serde = "1"

# Pin minor version - more conservative
sqlx = "0.7"

# Pin exact version - for critical dependencies
ring = "=0.17.7"

# Use caret (^) explicitly - same as default
regex = "^1.10"

# Minimum version - at least this version
# (rarely used, usually for bug fixes)
uuid = ">=1.6"

# Git dependencies - for unreleased fixes
# my-fork = { git = "https://github.com/me/crate", branch = "fix" }

# Path dependencies - for local development
# my-lib = { path = "../my-lib" }

# Workspace dependencies - centralized management
[dependencies.common]
workspace = true

# Feature selection - only enable what you need
[dependencies.reqwest]
version = "0.11"
default-features = false
features = ["json", "rustls-tls"]

# Optional dependencies for features
[dependencies.tracing]
version = "0.1"
optional = true

[dev-dependencies]
# Test dependencies - only for tests
tokio-test = "0.4"
mockall = "0.12"
criterion = "0.5"

[build-dependencies]
# Build script dependencies
tonic-build = "0.10"

[features]
default = []
tracing = ["dep:tracing"]
# Cargo.lock - Commit for applications, not for libraries
# This file is auto-generated

[[package]]
name = "tokio"
version = "1.35.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abc123..."
dependencies = [
 "bytes",
 "mio",
 "parking_lot",
]
// src/lib.rs - Dependency usage patterns
//! Library demonstrating good dependency practices

// Re-export key types users will need
pub use uuid::Uuid;

// Hide internal dependency details
mod http {
    use reqwest::Client;

    pub struct HttpClient {
        inner: Client,
    }

    impl HttpClient {
        pub fn new() -> Self {
            HttpClient {
                inner: Client::new(),
            }
        }
    }

    impl Default for HttpClient {
        fn default() -> Self {
            Self::new()
        }
    }
}

// Use dependency types internally, expose your own types
pub mod api {
    use serde::{Deserialize, Serialize};

    /// Your public type, not serde's
    #[derive(Debug, Clone, Serialize, Deserialize)]
    pub struct User {
        pub id: u64,
        pub name: String,
    }
}
# .cargo/config.toml - Project-wide cargo configuration

[build]
# Use all CPU cores
jobs = 0

[net]
# Retry network operations
retry = 3

[registries]
# Private registry configuration
my-company = { index = "https://cargo.mycompany.com/index" }

[source.crates-io]
# Mirror for reliability
replace-with = "mirror"

[source.mirror]
registry = "https://mirror.example.com/crates.io-index"
# .github/workflows/audit.yml - Security auditing
name: Security Audit

on:
  push:
    paths:
      - '**/Cargo.toml'
      - '**/Cargo.lock'
  schedule:
    - cron: '0 0 * * *'  # Daily

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install cargo-audit
        run: cargo install cargo-audit

      - name: Run security audit
        run: cargo audit

      - name: Check for outdated dependencies
        run: |
          cargo install cargo-outdated
          cargo outdated --exit-code 1
// build.rs - Version checking at build time
fn main() {
    // Verify minimum rustc version
    let version = rustc_version::version().unwrap();
    assert!(
        version >= rustc_version::Version::new(1, 70, 0),
        "Rust 1.70+ required"
    );

    // Generate version info
    println!(
        "cargo:rustc-env=BUILD_VERSION={}",
        std::env::var("CARGO_PKG_VERSION").unwrap()
    );
}
// Conditional compilation based on dependency features
#[cfg(feature = "tracing")]
use tracing::{info, instrument};

pub struct Service {
    name: String,
}

impl Service {
    #[cfg_attr(feature = "tracing", instrument)]
    pub fn process(&self, input: &str) -> String {
        #[cfg(feature = "tracing")]
        info!("Processing input");

        format!("{}: {}", self.name, input)
    }
}
#!/bin/bash
# scripts/dep-check.sh - Dependency health check

set -e

echo "=== Dependency Health Check ==="

# Check for security vulnerabilities
echo "Checking security vulnerabilities..."
cargo audit

# Check for outdated dependencies
echo "Checking for outdated dependencies..."
cargo outdated

# Check for unused dependencies
echo "Checking for unused dependencies..."
cargo +nightly udeps

# Check dependency tree for duplicates
echo "Checking for duplicate dependencies..."
cargo tree -d

# Check total dependency count
echo "Dependency statistics:"
cargo tree --depth 0 | wc -l

echo "=== Check Complete ==="

Why This Works

  1. Semantic versioning: Predictable update behavior
  2. Lock file: Reproducible builds
  3. Security audits: Catch vulnerabilities early
  4. Feature selection: Minimal dependency surface

Dependency Commands

# Add dependency
cargo add tokio --features full

# Add dev dependency
cargo add mockall --dev

# Update to latest compatible
cargo update

# Update specific crate
cargo update -p tokio

# Show dependency tree
cargo tree

# Show duplicates
cargo tree -d

# Show why a crate is included
cargo tree -i ring

# Check for vulnerabilities
cargo audit

# Check for outdated
cargo outdated

Version Specification

| Syntax | Meaning | Example |

|--------|---------|---------|

| 1.2.3 | ^1.2.3 (default) | >= 1.2.3, < 2.0.0 |

| ^1.2.3 | Compatible | >= 1.2.3, < 2.0.0 |

| ~1.2.3 | Patch only | >= 1.2.3, < 1.3.0 |

| =1.2.3 | Exact | = 1.2.3 only |

| >=1.2.3 | Minimum | >= 1.2.3 |

| * | Any | (avoid!) |

⚠️ Anti-patterns

# DON'T: Wildcard versions
tokio = "*"  # Unpredictable!

# DON'T: Overly restrictive
serde = "=1.0.193"  # Can't get security fixes

# DON'T: Git deps in published crates
# my-crate = { git = "..." }  # Won't publish!

# DON'T: Enable all features by default
reqwest = { version = "0.11", features = ["full"] }

# DO: Specify only needed features
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] }
// DON'T: Expose dependency types in your API
pub fn get_client() -> reqwest::Client { }  // Leaks dependency!

// DO: Wrap or re-export explicitly
pub struct Client(reqwest::Client);
pub fn get_client() -> Client { }

// Or if intentional, re-export
pub use reqwest::Client;  // Explicit re-export

Exercises

  1. Set up cargo-deny for license and security checking
  2. Create a workspace with shared dependency versions
  3. Implement build-time version compatibility checks
  4. Add automated dependency update PRs with Dependabot/Renovate

🎮 Try it Yourself

🎮

Dependency Management - Playground

Run this code in the official Rust Playground