- name
- rust-workspace
- license
- Apache-2.0
- description
- Design Rust project topology — single-crate packages, project-sized multi-crate workspaces (small root-flat, hybrid/domain-grouped, contained crates/, nested, or root-package layouts), workspace-level configuration, dependency DAGs, and crate-boundary decisions. Use when users ask how to split a project into crates, choose paths from the resulting project scale instead of copying a source-language module tree, configure a workspace, avoid dependency cycles, refactor a mixed root-package workspace, or decide between modules and crates. For in-crate src/ layout, see rust-module-layout.
# Rust Workspace and Project Topology
> Authority: [Cargo Book — Workspaces](https://doc.rust-lang.org/cargo/reference/workspaces.html), [The Book ch7](https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html) and [ch14-03](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html), [Rust Reference ch7](https://doc.rust-lang.org/reference/items/modules.html), [RFC 1525](https://rust-lang.github.io/rfcs/1525-cargo-workspace.html), [matklad — Large Rust Workspaces](https://matklad.github.io/2021/08/22/large-rust-workspaces.html).
This skill decides **how many crates a project should have and how they relate**. Its companion `rust-module-layout` decides **what lives inside one crate's `src/`**.
## Capability Boundaries
### ✅ Strengths
1. Single-crate vs multi-crate workspace decision (decision tree with five concrete triggers)
2. Project-driven workspace layouts: small root-flat, hybrid/domain-grouped, contained `crates/`, nested, and root-package
3. Virtual manifest vs root package — trade-offs and migration
4. Workspace-level shared configuration: `[workspace.package]`, `[workspace.dependencies]`, `[workspace.lints]`
5. Dependency direction DAGs — types → core → sdk → server → binary
6. Diagnosing and refactoring the "mixed root package" anti-pattern
7. Crate naming, publishing, and version coordination across members
### ⚠️ Prerequisites
1. Rust ownership and basic module syntax — see `rust-stable`
2. In-crate module layout (lib.rs facade, mod declarations, visibility) — see `rust-module-layout`
### ❌ Out of Scope
1. Cargo.toml `[dependencies]` syntax and feature resolution → use `rust-cargo-build`
2. In-crate src/ directory layout → use `rust-module-layout`
3. Rust syntax fundamentals → use `rust-stable`
4. Testing organization → use `rust-testing`
## When to Use
- "Should I split this into a workspace or keep it one crate?"
- "How do I configure a Cargo workspace?"
- "Refactor my rbatis-style mixed root package"
- "Why does cargo only build one crate at the root?"
- "How do I share dependencies across workspace members?"
- "Is my dependency direction correct?"
## Data Privacy
This skill does not collect, store, or transmit any user data.
---
# Foundations — Packages, Crates, Modules
> In-crate module layout depth (mod files, visibility, re-exports) is covered by the companion `rust-module-layout` skill. This section covers only the parts that affect project-level decisions.
### Package vs crate vs workspace
| Term | Meaning |
|------|---------|
| **Package** | One `Cargo.toml` and the source it points at; what you publish to crates.io |
| **Crate** | A compilation unit — either a library (`src/lib.rs`) or a binary (`src/main.rs`, `src/bin/*.rs`). A package can contain multiple crates (1 lib + N binaries). |
| **Workspace** | A collection of packages sharing one `Cargo.lock` and `target/`. |
| **Module** | A nameable scope inside a crate — declared with `mod foo;`, resolved to `src/foo.rs` or `src/foo/mod.rs`. |
### Single-package layouts
```text
# Library only # Binary only # Library + binary
my-lib/ my-app/ my-crate/
├── Cargo.toml ├── Cargo.toml ├── Cargo.toml
└── src/ └── src/ └── src/
└── lib.rs └── main.rs ├── lib.rs
└── main.rs
# Multi-binary (one package, multiple binaries)
my-app/
├── Cargo.toml
└── src/
├── lib.rs
├── main.rs # binary named after the package
└── bin/
├── tool_a.rs # binary `tool_a`
└── tool_b.rs # binary `tool_b`
```
### Module declaration (the rule that surprises Java/Python devs)
```rust
// src/lib.rs — declare modules explicitly; directories are NOT auto-discovered
pub mod front_of_house; // loads src/front_of_house.rs OR src/front_of_house/mod.rs
mod back_of_house; // private
pub(crate) mod utils; // crate-visible
```
Everything is **private by default**; `pub` exposes. See `rust-module-layout` for the full visibility model, the parent-bound reachability rule, and the modern `foo.rs + foo/` layout.
### `use` paths (quick reference)
```rust
use crate::front_of_house::hosting; // absolute (current crate)
use std::collections::HashMap; // absolute (external)
use self::back_of_house::Cook; // relative (current module)
use super::parent_module::helper; // relative (parent)
use std::{cmp::Ordering, io}; // nested
use std::fmt::Result as FmtResult; // alias
pub use crate::front_of_house::hosting; // re-export (facade pattern)
```
### Conditional compilation
```rust
#[cfg(target_os = "linux")]
fn only_linux() {}
#[cfg(feature = "serde")]
fn with_serde() {}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
struct Config;
if cfg!(target_os = "linux") { /* runtime check */ }
```
# Project Scaffolding
```bash
cargo new my-app # Binary project
cargo new my-lib --lib # Library project
cargo init # Initialize current directory
```
For multi-crate workspaces, create the root virtual manifest by hand (there is no `cargo workspace new`), choose the member topology from the decision model below, then run `cargo new --lib <selected-member-path>`. For a small root-flat workspace that may be `cargo new --lib my-core`; for a contained large workspace it may be `cargo new --lib crates/my-core`. For templates, use [`cargo-generate`](https://github.com/cargo-generate/cargo-generate).
---
# Workspace Patterns (single-crate vs multi-crate)
> Authority: [Cargo Book — Workspaces](https://doc.rust-lang.org/cargo/reference/workspaces.html), [The Book ch14-03](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html), [matklad — Large Rust Workspaces](https://matklad.github.io/2021/08/22/large-rust-workspaces.html), [RFC 1525](https://rust-lang.github.io/rfcs/1525-cargo-workspace.html).
A **workspace** is a collection of one or more packages that share a single `Cargo.lock` and `target/` directory. Workspaces exist for four reasons: **faster builds** (shared dependency compilation), **coordinated versions** (one lockfile, one `cargo publish` flow), **cleaner boundaries** (each crate is an independent compilation unit with its own public API), and **independent reuse** (users can depend on one crate without pulling in the others).
## Decision tree — how many crates?
```
Is this project one cohesive library or one binary?
├── Yes → Single-crate layout (Section 8.1)
└── No, it has multiple subdomains
├── Do the subdomains share types and call each other heavily?
│ └── Single-crate with module directories (use rust-module-layout skill)
└── At least one subdomain is independently useful / independently versioned / has different deps?
└── Multi-crate workspace (Section 8.2)
```
**Rule of thumb**: split into crates only when at least one of these is true (from `references/production-workspace-boundaries.md`):
1. The subdomain requires independent publishing or third-party reuse
2. The subdomain needs distinct `feature` / target / `no_std` / WASM boundaries
3. You need to **prohibit** a reverse dependency at compile time (e.g., `core` must not depend on `tokio`)
4. The subdomain has an independently versioned public API
5. Test/build lifecycles are significantly different
If none of these apply, **prefer modules over crates**. Modules are cheaper (no publish, no version coordination, no separate `Cargo.toml`). See the `rust-module-layout` skill for in-crate organization.
## Single-crate layout
The default for small-to-medium projects. One `Cargo.toml`, one `src/`. Four variants: library only, binary only, library + binary (same package), or multi-binary (`src/bin/<name>.rs`). See `examples/single-crate.md` for all four skeletons.
**When to graduate to a workspace**: when a binary or library needs an independent dependency/feature/target boundary, release trajectory, or build/test lifecycle. A second binary alone can remain under `src/bin/` in the same package.
## Multi-crate workspace — two flavors of root
Cargo supports two kinds of root `Cargo.toml`:
| Flavor | What's in root | When to use |
|--------|---------------|-------------|
| **Virtual manifest** | `[workspace]` only — **no `[package]`** | **Default for new workspaces.** Clean root, no top-level `src/`; members may be root-flat, hybrid, or contained. |
| **Root package** | `[workspace]` **+** `[package]` + `src/` at root | Small (2-3 package) workspaces where one package is unambiguously primary; you accept the trade-offs below. |
The Cargo team and community (notably matklad's [Large Rust Workspaces](https://matklad.github.io/2021/08/22/large-rust-workspaces.html)) recommend **virtual manifests** for any non-trivial workspace:
1. **Root pollution** — a `[package]` at the root forces `src/`, `tests/`, `benches/` into the top level alongside every other crate's directory.
2. **Command ergonomics** — with a root package, `cargo build` at the root builds *only* the root package; `--workspace` is needed for everything. Virtual manifests build all members by default.
3. **Publishing friction** — root package + members leads to confusing `cargo publish` ordering.
## Project-driven layout patterns
| Pattern | Layout | When | Used by |
|---------|--------|------|---------|
| **A. Root-flat virtual** | `<member>/` beside root `Cargo.toml` | **Default for small cohesive workspaces**, usually 2-8 packages/members | [Tokio](https://github.com/tokio-rs/tokio), [Serde](https://github.com/serde-rs/serde), [Clap](https://github.com/clap-rs/clap) |
| **B. Hybrid/domain-grouped** | core members at root plus `support/*`, `examples/*`, or another real family | Growing workspaces with stable families or noisy adapters/examples | framework and migration workspaces |
| **C. Contained/grouped** | `crates/<member>/` or `crates/<category>/*` | Large workspaces, multi-language repositories, or roots that need a Rust container | [Bevy](https://github.com/bevyengine/bevy)-style large repositories |
| **D. Nested sub-workspaces** | `vendor/<sub>/` with own `[workspace]` | Git submodule or vendored-workspace isolation (rare) | vendored upstream workspaces |
| **E. Root package** | `[package]` at root plus sibling/contained members | 2-3 packages with one genuinely primary crate | library plus companion CLI |
Workspace member count is a **signal, not a law**. Decide package/crate boundaries first, then consider repository root noise, independent publishing, adapter/plugin families, targets, examples/tests, other languages, and established paths. As a review trigger: 2-8 cohesive packages normally stay root-flat; around 8-20 compare root-flat with a hybrid; 20+ commonly benefits from grouping or `crates/`. Never create one Cargo package per Maven/Gradle module without a Rust boundary reason.
Full skeletons and the decision matrix: `references/workspace-layouts.md`.
### Pattern A skeleton — small workspace default
```text
my-project/
├── Cargo.toml # virtual: [workspace]
├── my-core/ # library
│ ├── Cargo.toml
│ └── src/lib.rs
├── my-net/ # library
│ └── ...
└── my-cli/ # binary
└── src/main.rs
```
```toml
# root Cargo.toml — virtual manifest
[workspace]
resolver = "3"
members = ["my-core", "my-net", "my-cli"]
[workspace.package]
edition = "2024"
version = "0.1.0"
license = "Apache-2.0"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] } # pin once
my-core = { path = "my-core" } # internal deps here
```
```toml
# my-net/Cargo.toml
[package]
name = "my-net"
version.workspace = true # inherits from [workspace.package]
edition.workspace = true
[dependencies]
serde.workspace = true # inherits from [workspace.dependencies]
my-core.workspace = true # internal workspace dep
```
## The "mixed root package" anti-pattern
A common mistake from developers coming from Java/Maven or Python: putting a real `[package]` with substantial code at the workspace root **while also** having member crates.
```toml
# Cargo.toml — DON'T (anonymized from a real public crate)
[workspace]
members = ["codegen", "macro-driver", "example"]
[package] # ❌ root package + workspace together
name = "my_framework"
version = "4.9.6"
[dependencies]
my-codegen = { path = "codegen" }
my-macro-driver = { path = "macro-driver" }
```
```text
my-framework/
├── Cargo.toml # 163 lines mixing workspace + package config
├── src/ # ❌ the root package's source pollutes the root
│ ├── lib.rs # 30 lines of `pub use *::*`
│ ├── crud.rs # 591 lines (monster file — see rust-module-layout)
│ ├── executor.rs # 642 lines
│ └── plugin/
├── codegen/ # member crate
├── macro-driver/ # member crate
└── tests/
```
**Five problems**: root pollution, asymmetric commands (`cargo build` builds only the root), coupled versioning, no clear facade (lib.rs becomes a glob re-export hub), and migration is a breaking change once published.
**Refactor target** — virtual manifest with all crates under `crates/`:
```text
my-framework/
├── Cargo.toml # virtual manifest — workspace config only
└── crates/
├── my-framework/ # was: src/ at root
│ ├── Cargo.toml
│ └── src/
├── codegen/
├── macro-driver/
└── example/
```
Migration is **mechanical and preserves the published crate name** — full six-step procedure in `references/mixed-root-package-antipattern.md` and a worked example in `examples/migration-mixed-to-virtual.md`.
## Workspace-level shared configuration
Ver en GitHub