Introduction
Multi-crate projects in Rust can quickly become unwieldy if you don't establish clear boundaries and shared dependency management from the start. Rust workspaces: concepts, trade-offs and production lessons—this is the pattern that helps teams scale a single-service architecture or a tightly-coupled monorepo without fracturing dependency versions or duplicating build artifacts. We've learned through production experience that workspace discipline pays dividends in build times, maintainability, and deployment confidence.
In this article, we'll explore how to structure and manage Rust workspaces effectively, drawing on patterns we've applied in API servers, domain logic libraries, and infrastructure utilities across our e-commerce and SaaS platforms.
Workspace Cargo.toml: The Entry Point
A Rust workspace is defined by a root Cargo.toml that declares which crates belong together. This root manifest doesn't define your application directly; instead, it orchestrates a collection of member crates, each with its own Cargo.toml.
Here's a typical structure for a backend service:
# Root Cargo.toml (in project-root/)
[workspace]
members = [
"crates/api",
"crates/domain",
"crates/infra",
"crates/common",
]
resolver = "2"
Each member crate lives in its own subdirectory and declares its own dependencies and build configuration. The resolver field (set to "2" for workspaces with interdependencies) ensures Rust's dependency resolution respects the workspace boundary.
When you run cargo build from the workspace root, Cargo discovers all members and builds them in dependency order. This single command replaces manual coordination of multiple crates, reducing friction for developers and CI/CD systems alike.
A concrete benefit: new team members check out one repository, run one build command, and immediately have a working local environment. No juggling multiple clone paths or remembering build order.
Shared Dependencies: workspace.dependencies for Version Control
Managing versions across multiple crates is error-prone if each crate pins its own copy. A crate using tokio = "1.35" while another uses tokio = "1.34" can introduce subtle bugs and bloat the dependency tree.
Workspaces solve this with a shared dependency table at the root level:
# Root Cargo.toml
[workspace]
members = ["crates/api", "crates/domain", "crates/infra"]
[workspace.dependencies]
tokio = { version = "1.40", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
sqlx = { version = "0.8", features = ["postgres", "runtime-tokio-rustls"] }
tracing = "0.1"
anyhow = "1.0"
Then, in each member crate's Cargo.toml, reference the workspace version:
# crates/domain/Cargo.toml
[package]
name = "domain"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { workspace = true }
serde = { workspace = true }
anyhow = { workspace = true }
This pattern gives you a single source of truth for dependency versions. Upgrading tokio is now one edit in the root file, and all member crates inherit that change immediately. In production, we've found this eliminates entire classes of version-mismatch bugs that would otherwise surface under load.
A practical note: workspace dependencies don't lock you into identical feature sets across all crates. If crates/infra needs extra features for a database driver while crates/domain doesn't, you can override locally without loss of consistency.
Split by Responsibility: API, Domain, and Infrastructure Crates
The architecture of your workspace should reflect your domain. A common pattern we use across projects separates concerns into distinct crates: one for HTTP/API logic, one for business domain, and one for infrastructure (database, cache, external integrations).
Here's how that looks in a ride-hailing backend:
| Crate | Responsibility | Typical Dependencies |
|---|---|---|
crates/api | HTTP handlers, request/response serialization, middleware | actix-web, serde, domain |
crates/domain | Business logic, data models, validation rules | serde, anyhow, no framework deps |
crates/infra | Database queries, Redis client, third-party API wrappers | sqlx, redis, reqwest, domain |
crates/common | Shared types, error handling, utilities | serde, thiserror (minimal framework deps) |
This separation enforces loose coupling. The domain crate has no knowledge of HTTP or web frameworks—it defines your business entities and workflows. The API crate depends on domain but stays lightweight. The infra crate provides data access without leaking SQL syntax into your handlers.
A practical advantage: you can test domain logic in isolation without spinning up a database or HTTP server. In a CRM backend we built, the domain crate's test suite runs in under 2 seconds, giving developers instant feedback during iteration.
When adding a new feature, the dependency graph guides your work: if your domain crate can't import from the API crate (due to circular dependencies), you've designed the boundary correctly. If it can, you've coupled concerns too tightly and should refactor.
Faster Builds: The Shared Target Directory
By default, a Rust workspace shares a single target directory at the root level. This is one of the biggest wins: compiled artifacts, intermediate objects, and incremental build state are pooled across all member crates.
Compare two scenarios:
- Without workspace: Three separate repositories, each with its own
targetdirectory, means duplicated compilation of shared dependencies. - With workspace: One
targetdirectory meanstokiocompiles once, used by all three crates.
In a real project with 15–20 dependencies, this difference is dramatic. We've measured clean builds dropping from 5–6 minutes to 2–3 minutes when consolidating three separate crates into a workspace. Incremental rebuilds after a small change go from 20–30 seconds to 3–5 seconds.
The shared target directory also improves disk usage. A CI/CD pipeline running workspace builds consumes significantly less storage and bandwidth when pulling cached artifacts.
One caveat: if a crate needs to expose a binary or library with a specific build configuration (e.g., release vs. debug, or architecture-specific flags), you can override the target directory per crate in rare cases. We haven't needed this in production, but the option exists.
Common Mistakes
Mistake 1: Over-splitting into too many crates. We've seen teams create a crate for every module, thinking it improves organization. The reality: compilation time increases due to inter-crate linking overhead, and the boundaries become arbitrary. We recommend starting with 3–4 crates (api, domain, infra, common) and splitting further only when clear architectural reasons emerge.
Mistake 2: Circular dependencies between crates. If api depends on domain, and domain tries to import from api, the build fails. This is actually good—it forces you to think about layering. If you hit this, extract the shared code into common. We've caught many design issues early thanks to this constraint.
Mistake 3: Ignoring workspace dependency versions. Manually pinning versions in each crate defeats the purpose. Use workspace.dependencies consistently, and treat the root Cargo.toml as your version policy document.
Mistake 4: Publishing all crates to crates.io indiscriminately. If your crates are internal to one service, don't publish them. Mark them as private in Cargo.toml, or use a path dependency. We've seen teams waste time managing semver for internal utilities that change weekly.
Key Takeaways
- Use a workspace root
Cargo.tomlto declare all member crates and centralize dependency versions with[workspace.dependencies]. - Structure crates by responsibility—separate API, domain business logic, and infrastructure concerns to enforce clean boundaries and improve testability.
- Leverage the shared target directory for faster builds and lower disk usage in both local and CI/CD environments.
- Avoid excessive crate splitting; start with 3–4 logical units and refactor only when clear architectural needs emerge.
- Let Rust's compiler enforce your architecture: if you can't build due to circular dependencies, you've likely coupled concerns too tightly.
- For large teams, document your workspace structure and crate responsibilities in a CONTRIBUTING guide; consistency across the org pays off.






Comments
Loading comments…