Skip to main content
Technology & EngineeringRust320 lines

Cargo Workspace

Cargo workspaces, project structure, dependency management, and multi-crate Rust project organization

Quick Summary34 lines
You are an expert in Cargo workspaces and Rust project organization for building well-structured, maintainable multi-crate projects.

## Key Points

- Use `workspace.dependencies` to centralize dependency versions across the workspace. Members use `dep.workspace = true` to inherit.
- Structure crates by responsibility: `core` for domain types and traits, separate crates for each infrastructure concern (database, HTTP, CLI).
- Keep the dependency graph acyclic and one-directional: `cli -> api -> core`, `db -> core`. Never let `core` depend on `api` or `db`.
- Use feature flags to make heavy or optional dependencies opt-in rather than compiling them always.
- Pin your `rust-toolchain.toml` for reproducible builds:
- Use `cargo deny` to audit licenses and detect duplicate dependencies.
- Commit `Cargo.lock` for binaries and applications; omit it for libraries.
- **Circular dependencies**: Workspace members cannot depend on each other cyclically. Extract shared types into a `core` or `common` crate.
- **Version conflicts**: Without `workspace.dependencies`, different crates can pull in incompatible versions of the same dependency, causing confusing compile errors.
- **Large rebuild radius**: If `core` changes frequently and everything depends on it, every change triggers a rebuild of the entire workspace. Keep `core` stable and lean.
- **Publishing workspace crates**: When publishing to crates.io, path dependencies must also specify a version. Use `myapp-core = { path = "../core", version = "0.1" }`.
- **Unused dependencies**: Use `cargo machete` or `cargo udeps` to find and remove unused dependencies that slow down compilation.

## Quick Example

```rust
// Conditional compilation based on features
#[cfg(feature = "json")]
pub fn parse_json(input: &str) -> Result<Value, serde_json::Error> {
    serde_json::from_str(input)
}
```

```toml
[toolchain]
channel = "1.77.0"
components = ["rustfmt", "clippy"]
```
skilldb get rust-skills/Cargo WorkspaceFull skill: 320 lines
Paste into your CLAUDE.md or agent config

Cargo & Workspaces — Rust Programming

You are an expert in Cargo workspaces and Rust project organization for building well-structured, maintainable multi-crate projects.

Core Philosophy

Cargo is not just a build tool -- it is the backbone of how Rust projects are structured, shared, and maintained. A well-organized Cargo workspace reflects the architecture of your system: each crate maps to a bounded responsibility, dependency edges are explicit and one-directional, and shared configuration eliminates version drift. The workspace is your module boundary enforced by the compiler.

The principle behind workspace.dependencies and the resolver is reproducibility. Every build of the same lock file should produce the same binary. When you centralize dependency versions in the workspace root and commit Cargo.lock for applications, you eliminate an entire class of "works on my machine" bugs. Feature flags extend this control, letting you compile only the code paths a given binary actually needs.

Structure decisions made early -- where to split crates, how to layer dependencies, when to introduce feature flags -- compound over the life of a project. A flat workspace with too few crates leads to long recompile times and tangled concerns. Too many crates create a maze of Cargo.toml files and path dependencies. Aim for crates that change at different rates to live in different packages, and keep the dependency graph shallow.

Anti-Patterns

  • Monolith crate with module-only separation: Keeping everything in a single crate with mod boundaries means every change recompiles everything. Split crates along stability boundaries so that core types change rarely and leaf crates iterate fast.
  • Circular or bidirectional workspace dependencies: If crate-a depends on crate-b and vice versa, the design is wrong. Extract shared types into a core or common crate and make the dependency graph strictly acyclic.
  • Divergent dependency versions across members: Without workspace.dependencies, two crates can pull different versions of serde or tokio, causing duplicate compilation and confusing linker errors. Centralize all shared dependency versions in the workspace root.
  • Omitting resolver = "2" in workspaces: Resolver 1 merges dev-dependency features into normal builds, activating code paths you never intended. Always set resolver = "2" to get correct per-context feature resolution.
  • Committing Cargo.lock for libraries but not for applications: Libraries should let downstream consumers resolve versions, so they omit the lock file. Applications need reproducible builds, so they commit it. Getting this backwards causes either broken CI or unnecessary lock file churn in library repos.

Overview

Cargo is Rust's build system and package manager. Workspaces allow multiple related crates (packages) to share a single Cargo.lock, output directory, and build configuration. A well-structured workspace separates concerns into focused crates, speeds up incremental compilation, and enforces clean dependency boundaries.

Core Concepts

Single Crate Structure

my-project/
├── Cargo.toml
├── src/
│   ├── main.rs        # binary entry point
│   ├── lib.rs         # library root
│   └── utils.rs       # module
├── tests/
│   └── integration.rs # integration tests
├── benches/
│   └── benchmark.rs   # benchmarks
└── examples/
    └── demo.rs        # runnable examples

Workspace Setup

Root Cargo.toml:

[workspace]
resolver = "2"
members = [
    "crates/core",
    "crates/api",
    "crates/cli",
    "crates/db",
]

# Shared dependencies across workspace
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"
tracing = "0.1"

Member crate crates/api/Cargo.toml:

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

[dependencies]
# Inherit from workspace
serde.workspace = true
tokio.workspace = true
anyhow.workspace = true

# Local workspace dependency
myapp-core = { path = "../core" }

# Crate-specific dependency
axum = "0.7"

Recommended Workspace Layout

my-workspace/
├── Cargo.toml              # workspace root
├── Cargo.lock              # shared lock file
├── crates/
│   ├── core/               # shared types, traits, domain logic
│   │   ├── Cargo.toml
│   │   └── src/
│   │       └── lib.rs
│   ├── db/                 # database layer
│   │   ├── Cargo.toml
│   │   └── src/
│   │       └── lib.rs
│   ├── api/                # HTTP API server
│   │   ├── Cargo.toml
│   │   └── src/
│   │       ├── lib.rs
│   │       └── main.rs
│   └── cli/                # CLI binary
│       ├── Cargo.toml
│       └── src/
│           └── main.rs
├── tests/                  # workspace-level integration tests
└── .cargo/
    └── config.toml         # Cargo configuration

Implementation Patterns

Module Organization Within a Crate

// src/lib.rs
pub mod config;
pub mod error;
pub mod models;
pub mod services;

// Re-export key types at the crate root
pub use config::Config;
pub use error::AppError;
// src/models/mod.rs
mod user;
mod order;

pub use user::User;
pub use order::Order;

Feature Flags

[package]
name = "myapp-core"

[features]
default = ["json"]
json = ["serde_json"]
yaml = ["serde_yaml"]
full = ["json", "yaml"]

[dependencies]
serde_json = { version = "1", optional = true }
serde_yaml = { version = "0.9", optional = true }
// Conditional compilation based on features
#[cfg(feature = "json")]
pub fn parse_json(input: &str) -> Result<Value, serde_json::Error> {
    serde_json::from_str(input)
}

Workspace-Level Configuration

.cargo/config.toml:

[build]
# Use mold linker for faster linking (Linux)
# rustflags = ["-C", "link-arg=-fuse-ld=mold"]

[alias]
xtask = "run --package xtask --"

[profile.dev]
opt-level = 1

[profile.dev.package."*"]
# Optimize dependencies even in dev for faster runtime
opt-level = 2

Cargo Profiles

In the workspace root Cargo.toml:

[profile.release]
lto = true          # link-time optimization
codegen-units = 1   # single codegen unit for maximum optimization
strip = true        # strip debug symbols
panic = "abort"     # smaller binary, no unwinding

[profile.dev]
debug = true
incremental = true

# Custom profile for CI tests
[profile.ci]
inherits = "release"
debug = true        # keep debug info for backtraces
strip = false

Multi-Binary Crate

[package]
name = "myapp"

[[bin]]
name = "server"
path = "src/bin/server.rs"

[[bin]]
name = "worker"
path = "src/bin/worker.rs"

[[bin]]
name = "migrate"
path = "src/bin/migrate.rs"

The xtask Pattern

A build automation crate within the workspace, replacing Makefiles:

# crates/xtask/Cargo.toml
[package]
name = "xtask"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = { version = "4", features = ["derive"] }
// crates/xtask/src/main.rs
use clap::Parser;

#[derive(Parser)]
enum Command {
    /// Run database migrations
    Migrate,
    /// Generate OpenAPI spec
    Openapi,
    /// Build and package for release
    Dist,
}

fn main() -> anyhow::Result<()> {
    let cmd = Command::parse();
    match cmd {
        Command::Migrate => run_migrations(),
        Command::Openapi => generate_openapi(),
        Command::Dist => build_dist(),
    }
}

Run with: cargo xtask migrate

Workspace Commands

# Build everything
cargo build --workspace

# Test everything
cargo test --workspace

# Run a specific binary
cargo run -p myapp-api

# Check a specific crate
cargo check -p myapp-core

# Run clippy on the entire workspace
cargo clippy --workspace -- -D warnings

# Format everything
cargo fmt --all

Best Practices

  • Use workspace.dependencies to centralize dependency versions across the workspace. Members use dep.workspace = true to inherit.
  • Structure crates by responsibility: core for domain types and traits, separate crates for each infrastructure concern (database, HTTP, CLI).
  • Keep the dependency graph acyclic and one-directional: cli -> api -> core, db -> core. Never let core depend on api or db.
  • Use feature flags to make heavy or optional dependencies opt-in rather than compiling them always.
  • Pin your rust-toolchain.toml for reproducible builds:
[toolchain]
channel = "1.77.0"
components = ["rustfmt", "clippy"]
  • Use cargo deny to audit licenses and detect duplicate dependencies.
  • Commit Cargo.lock for binaries and applications; omit it for libraries.

Common Pitfalls

  • Circular dependencies: Workspace members cannot depend on each other cyclically. Extract shared types into a core or common crate.
  • Version conflicts: Without workspace.dependencies, different crates can pull in incompatible versions of the same dependency, causing confusing compile errors.
  • Large rebuild radius: If core changes frequently and everything depends on it, every change triggers a rebuild of the entire workspace. Keep core stable and lean.
  • Forgetting resolver = "2": Workspaces default to resolver 1 for backward compatibility. Resolver 2 handles features more correctly (separates dev-dependency features from normal features). Always set it.
  • Publishing workspace crates: When publishing to crates.io, path dependencies must also specify a version. Use myapp-core = { path = "../core", version = "0.1" }.
  • Unused dependencies: Use cargo machete or cargo udeps to find and remove unused dependencies that slow down compilation.

Install this skill directly: skilldb add rust-skills

Get CLI access →