Rust Cargo Workspaces Tutorial
Organize multi-crate Rust projects with Cargo workspaces: shared dependencies, internal crates, build performance tips, and publishing workflows.
What you'll learn
- ✓When a workspace beats a single crate
- ✓How to lay out workspace members and shared metadata
- ✓Sharing dependencies via the workspace.dependencies table
- ✓Path dependencies vs version dependencies
- ✓Publishing individual crates from a workspace
Prerequisites
- •Comfort building a single-crate Rust project
What and Why
A Cargo workspace lets several related crates share a single Cargo.lock, build directory, and dependency resolution. As your codebase grows past a single binary, splitting it into a library crate, a binary crate, and maybe a CLI feels natural. A workspace makes that split painless.
The wins are real: faster builds (the shared target/ dedupes compilation), easier dependency upgrades, and a clean separation between reusable logic and the application that wires it up.
Mental Model
Picture a top-level Cargo.toml that names a list of member directories. Each member is a regular crate, but they all resolve dependencies together.
my-app/
Cargo.toml # [workspace] members = ["core","cli","web"]
Cargo.lock # shared
target/ # shared
core/
Cargo.toml # library crate
src/lib.rs
cli/
Cargo.toml # binary crate, depends on core
src/main.rs
web/
Cargo.toml # binary crate, depends on core
src/main.rs Hands-on Example
Top-level Cargo.toml:
[workspace]
resolver = "2"
members = ["core", "cli", "web"]
[workspace.package]
version = "0.1.0"
edition = "2021"
license = "MIT"
repository = "https://example.com/my-app"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"
A member crate inherits the shared metadata and dependencies:
# core/Cargo.toml
[package]
name = "my-core"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
serde = { workspace = true }
anyhow = { workspace = true }
A binary crate depends on the core library by path:
# cli/Cargo.toml
[package]
name = "my-cli"
version.workspace = true
edition.workspace = true
[dependencies]
my-core = { path = "../core" }
anyhow = { workspace = true }
tokio = { workspace = true }
Building or testing anything in the workspace is a single command:
cargo build --workspace
cargo test --workspace
cargo run -p my-cli -- --help
The -p (package) flag is how you target a single member from the root.
Common Pitfalls
- Forgetting
resolver = "2": workspaces default to resolver 1 in older Cargo, which causes feature unification surprises across binaries and dev-dependencies. Always setresolver = "2"for new workspaces. - Mixing path-only and version dependencies for publishing: a path dependency is fine locally but won’t publish to crates.io. Combine both:
my-core = { path = "../core", version = "0.1" }. When publishing, the version is used; locally, the path wins. - Feature unification across binaries: if
clienables a feature on a dependency,webgets the same feature flags during a workspace build. Sometimes you want this; sometimes you do not. Be deliberate. - Bloated
target/directories: sharedtarget/is fast but can grow quickly. Usecargo clean -p <crate>to clean a single member. - Implicit dev-dependencies: every member crate’s dev-deps still flow into the lockfile. Keep them tight.
Practical Tips
Put truly shared crate metadata in [workspace.package] and use field.workspace = true to inherit it. Version bumps then happen in one place.
For internal-only crates, mark them as not publishable with publish = false. This prevents accidental release and lets you skip version bumps for them.
When a workspace gets large, group members in subfolders and list globs:
[workspace]
members = ["crates/*", "tools/*"]
For CI, cargo build --workspace --all-targets and cargo test --workspace cover most ground. Add cargo clippy --workspace --all-targets -- -D warnings to catch lints across the whole project.
Use cargo-hakari or cargo-nextest when builds and tests slow down. Hakari maintains a workspace-wide hack crate that improves feature unification; nextest parallelizes tests far more aggressively than the default runner.
Wrap-up
Cargo workspaces are how Rust projects scale past a single crate without losing the consistency that makes Cargo pleasant. Start with a core library and one or two binaries, share dependencies through workspace.dependencies, and lean on -p to operate on individual members. By the time your project has a dozen crates, you will already have the muscle memory for fast, deterministic, multi-crate development.
Related articles
- Rust Rust Clippy and Fmt Tutorial
How to use Clippy and rustfmt effectively in Rust projects, including configuration, CI integration, and tips to make linting friction-free.
- Rust Cargo Basics: Building, Testing, and Publishing Rust Code
A practical tour of Cargo — creating projects, managing dependencies and features, running tests, building release binaries, and using workspaces.
- Rust Rust Actix vs Axum Comparison
A side-by-side comparison of Actix Web and Axum, covering architecture, ergonomics, performance, ecosystem, and how to pick the right one for your project.
- Rust Rust async/await and Futures Explained
How Rust's async/await desugars into a state machine, what a Future actually is, and the runtime model that makes it efficient on real workloads.