A Cargo workspace doesn’t actually do anything to your code itself; it’s purely a build-time optimization and organizational tool that lets you manage multiple related Rust crates as if they were one project.
Here’s a simple workspace in action. Imagine you have a library my_lib and an application my_app that uses it.
# workspace.toml
[workspace]
members = [
"my_lib",
"my_app",
]
Then, in your my_lib directory:
# my_lib/Cargo.toml
[package]
name = "my_lib"
version = "0.1.0"
edition = "2021"
[dependencies]
And in your my_app directory:
# my_app/Cargo.toml
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"
[dependencies]
my_lib = { path = "../my_lib" }
When you’re in the root directory (where workspace.toml lives) and run cargo build, Cargo will build both my_lib and my_app. If you run cargo build --release, it builds both in release mode. Crucially, if you run cargo test, it will run tests for all members of the workspace. This is the core power: a single command to manage the entire monorepo.
The primary problem Cargo workspaces solve is managing dependencies and builds across multiple related Rust projects that would otherwise be separate repositories or deeply entangled within a single src directory. It allows for shared development, testing, and publishing of these crates while maintaining clear separation. Internally, Cargo treats the workspace as a single build unit. It aggregates all dependencies, resolves them once, and then builds each member crate. This avoids redundant dependency resolution and compilation, especially when crates share common dependencies. The members field in workspace.toml is the sole configuration point; it tells Cargo which directories contain crates that belong to this workspace.
You control the structure by how you organize directories and which crates you list in the members array. Common patterns emerge from this flexibility:
-
Library and Applications: The most basic pattern, as shown above. A core library (
my_lib) used by one or more applications (my_app_cli,my_app_web).. ├── workspace.toml ├── my_lib/ │ ├── Cargo.toml │ └── src/ ├── my_app_cli/ │ ├── Cargo.toml │ └── src/ └── my_app_web/ ├── Cargo.toml └── src/This is excellent for isolating shared logic and testing it independently of any specific application.
-
Shared Utilities/Components: Multiple libraries that depend on each other, or provide common functionality. For instance,
network,database,auth, all used by various applications.. ├── workspace.toml ├── network/ │ ├── Cargo.toml │ └── src/ ├── database/ │ ├── Cargo.toml │ └── src/ ├── auth/ │ ├── Cargo.toml │ └── src/ ├── service_a/ │ ├── Cargo.toml │ └── src/ # depends on network, auth └── service_b/ ├── Cargo.toml └── src/ # depends on database, authThis pattern encourages modularity and reuse. Dependencies are declared using
pathin the respectiveCargo.tomlfiles, pointing to sibling directories. -
Feature-Rich Crate with Examples/Tests: A single large crate, with separate directories for examples or integration tests that exercise its features.
. ├── workspace.toml ├── my_awesome_crate/ │ ├── Cargo.toml │ └── src/ ├── examples/ │ ├── example_one/ │ │ ├── Cargo.toml # depends on my_awesome_crate │ │ └── src/ │ └── example_two/ │ ├── Cargo.toml # depends on my_awesome_crate │ └── src/Here, the
examplesdirectory might contain subdirectories, each being its own "package" within the workspace. TheirCargo.tomlfiles would listmy_awesome_crateas a dependency. -
Monorepo for a Company/Organization: A collection of independent crates that are all developed and maintained by the same team, potentially with shared tooling or CI.
. ├── workspace.toml ├── utils/ ├── api_gateway/ ├── user_service/ └── billing_service/This is less about tight coupling and more about centralized management.
When you run cargo build in the workspace root, Cargo builds all members. If you want to build only a specific member, you navigate into that member’s directory (e.g., cd my_app) and run cargo build there. However, even when building a member individually, Cargo is aware of the workspace context and will still use the aggregated dependency graph.
One subtlety often missed is how Cargo handles target directories in workspaces. By default, all build artifacts (compiled binaries, libraries, intermediate object files) for all members of a workspace are placed in a single target directory at the workspace root. This is a key part of the optimization: it prevents redundant compilations and ensures that if my_lib is updated, only the necessary dependent crates (my_app) are rebuilt, rather than having separate target directories for each. You can change this behavior using the target-dir field in the workspace’s Cargo.toml, but it’s rarely needed.
The next logical step after mastering workspace structure is understanding how to publish multiple crates from a workspace, which involves individual cargo publish commands for each crate but managed from the workspace root.