Cargo Workspace
Cargo workspaces, project structure, dependency management, and multi-crate Rust project organization
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 linesCargo & 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
modboundaries means every change recompiles everything. Split crates along stability boundaries so thatcoretypes change rarely and leaf crates iterate fast. - Circular or bidirectional workspace dependencies: If
crate-adepends oncrate-band vice versa, the design is wrong. Extract shared types into acoreorcommoncrate and make the dependency graph strictly acyclic. - Divergent dependency versions across members: Without
workspace.dependencies, two crates can pull different versions ofserdeortokio, 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 setresolver = "2"to get correct per-context feature resolution. - Committing
Cargo.lockfor 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.dependenciesto centralize dependency versions across the workspace. Members usedep.workspace = trueto inherit. - Structure crates by responsibility:
corefor 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 letcoredepend onapiordb. - Use feature flags to make heavy or optional dependencies opt-in rather than compiling them always.
- Pin your
rust-toolchain.tomlfor reproducible builds:
[toolchain]
channel = "1.77.0"
components = ["rustfmt", "clippy"]
- Use
cargo denyto audit licenses and detect duplicate dependencies. - Commit
Cargo.lockfor binaries and applications; omit it for libraries.
Common Pitfalls
- Circular dependencies: Workspace members cannot depend on each other cyclically. Extract shared types into a
coreorcommoncrate. - Version conflicts: Without
workspace.dependencies, different crates can pull in incompatible versions of the same dependency, causing confusing compile errors. - Large rebuild radius: If
corechanges frequently and everything depends on it, every change triggers a rebuild of the entire workspace. Keepcorestable 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 macheteorcargo udepsto find and remove unused dependencies that slow down compilation.
Install this skill directly: skilldb add rust-skills
Related Skills
Async Rust
Async Rust programming with async/await, Tokio runtime, futures, and concurrent task patterns
Error Handling
Rust error handling with Result, Option, the ? operator, and ecosystem crates anyhow and thiserror
Lifetimes
Rust lifetime annotations for ensuring reference validity and understanding the borrow checker
Ownership Borrowing
Rust ownership, borrowing, and move semantics for writing memory-safe code without a garbage collector
Pattern Matching
Rust pattern matching with match, if let, while let, destructuring, and advanced match patterns
Smart Pointers
Rust smart pointers Box, Rc, Arc, RefCell, and their combinations for heap allocation and shared ownership