When a crate should become a workspace
The trigger for splitting a workspace is build time and dependency boundaries, not tidy directories. Split when three signals hold, or you trade one compile for managing several packages.
Whether to split a workspace should not be an aesthetic decision. Tidier directories buy nothing if the build does not get faster and the dependency sets do not get cleaner; splitting only adds management cost.
Three signals
- Touching one place rebuilds everything. You change CLI argument parsing and recompile the whole crate, including the core library with hundreds of dependencies. Broken incremental builds are the first real reason to split.
- Dependency sets diverge. One side needs the full tokio runtime, an HTTP client and TLS; the other is a parser of pure functions. In one crate, users of the second are forced to pull in all of the first.
- A stable subset has to be exposed. The core library is for external users as a library while the CLI is an internal tool. Inside one crate, internals drift into
pubbecause it is convenient.
If none of the three holds, do not split. A crate of a few dozen source files builds in seconds. After splitting, cargo test runs across four packages, versions move together, and module paths become dep::foo instead of crate::foo.
How to split
The root Cargo.toml holds only members and shared versions:
[workspace]
members = ["crates/core", "crates/protocol", "crates/cli"]
resolver = "2"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
Members write serde.workspace = true, and internal dependencies use path. Upgrading one dependency then takes a single edit, which is the one thing guaranteed to improve.
Four problems that only appear after splitting
- Circular dependencies have no elegant fix. When A needs B’s types and B needs A’s, the only move is another crate C for shared types, turning the graph into a diamond. Two or more of those mean the split line is in the wrong place.
pubgets out of hand. Across crates everything must bepub, and thepub(crate)boundary disappears. Internals leaking into the public API is a matter of time, held back only by#[doc(hidden)]and deliberate module organisation.- Internal crates should not be published. One line,
publish = false, avoids a lot of version synchronisation. Only the crate meant for external users needs a release. - Features explode combinatorially. An optional dependency of A becomes a feature of B, and B’s feature has to be forwarded to C. Prefer not to forward, and let the top layer depend explicitly.
The test I use
It is a plain one: if you can write a sentence for what happens without the split, split; if you cannot, do not. Because changing the CLI would mean waiting forty seconds is a reason. Because it is cleaner is not.
Split files into modules first. Split crates when build time starts changing how often you save.

Comments
…