Monorepository with Rust: Code Organization in Practice
A monorepository (monorepo) is an approach to code management where multiple projects or components are stored in a single repository. In the Rust ecosystem, this practice is especially popular due to the built-in support of Cargo Workspace. In this article, we will explore how to organize a monorepository with Rust in practice: from creating the structure to managing dependencies and testing. You will learn how to avoid common mistakes and make your codebase scalable.
What is Cargo Workspace and Why You Need It
Cargo Workspace is a mechanism that allows you to manage multiple crates (libraries or executables) within a single project. Instead of creating separate repositories for each component, you combine them into a unified structure. This provides several advantages:
- Unified build system — a single
cargo buildcommand builds all crates. - Shared dependencies — duplicate dependencies are resolved once, reducing the final binary size.
- Simplified testing — you can run tests for all crates with a single command.
- Version consistency — all components use the same versions of libraries.
A monorepository is especially useful for microservices, general-purpose libraries, or projects with a clear separation into layers (e.g., core, API, CLI).
Creating the Monorepository Structure
Let's start with a practical example. Suppose we are developing a task management system. We will have three crates:
core— a library with business logic.cli— an executable for the command line.server— a web server (executable).
First, create the root directory and the Cargo.toml file for the workspace:
mkdir task_managercd task_manager# Create the root Cargo.tomltouch Cargo.tomlIn the root Cargo.toml, specify the workspace:
[workspace]members = [ "core", "cli", "server",]Now create each crate using cargo new:
cargo new core --libcargo new clicargo new serverAfter this, the structure will look like this:
task_manager/├── Cargo.toml # Root workspace├── core/│ ├── Cargo.toml│ └── src/│ └── lib.rs├── cli/│ ├── Cargo.toml│ └── src/│ └── main.rs└── server/ ├── Cargo.toml └── src/ └── main.rsNote: the root Cargo.toml does not contain a [package] section, only [workspace]. This is important for correct operation.
Managing Dependencies Between Crates
One of the key tasks in a monorepository is correctly setting up dependencies between crates. In our example, cli and server depend on core. Let's specify this in their Cargo.toml files:
For cli/Cargo.toml:
[package]name = "cli"version = "0.1.0"edition = "2021"
[dependencies]core = { path = "../core" }clap = { version = "4.0", features = ["derive"] } # External dependency for CLIFor server/Cargo.toml:
[package]name = "server"version = "0.1.0"edition = "2021"
[dependencies]core = { path = "../core" }actix-web = "4" # External dependency for web serverIn the root Cargo.toml, you can also specify common dependencies if they are needed by all crates, but this is usually done individually. However, the workspace allows resolving version conflicts: if two crates use the same library, Cargo will automatically pull in one version