Skip to content
C Codeloom
Rust

Rust Cargo Workspaces Tutorial

Organize multi-crate Rust projects with Cargo workspaces: shared dependencies, internal crates, build performance tips, and publishing workflows.

·4 min read · By Codeloom
Intermediate 9 min read

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
Workspace layout

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 set resolver = "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 cli enables a feature on a dependency, web gets the same feature flags during a workspace build. Sometimes you want this; sometimes you do not. Be deliberate.
  • Bloated target/ directories: shared target/ is fast but can grow quickly. Use cargo 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.