Monorepository with Rust: Code Organization and Best Practices

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction



Monorepository (monorepo) is an approach to source code management where multiple projects (libraries, applications, services) are stored in a single repository. In the Rust ecosystem, this approach has received powerful support through the Cargo Workspace mechanism. In this article, we will analyze how to effectively organize a monorepo in Rust, examine the structure, dependency management, and best practices.



Why is a monorepo useful for Rust projects? It allows you to:

  • Reuse code between multiple crates (libraries and binaries).
  • Simplify versioning and CI/CD.
  • Ensure atomicity of changes (one commit can update multiple crates).
  • Reduce the complexity of dependency management.


Basics of Cargo Workspace



Cargo Workspace is a built-in Rust mechanism for managing a monorepo. It combines multiple crates under one common top-level Cargo.toml file.



Creating a workspace



To begin, let's create a project directory and a Cargo.toml file in the root:



# Cargo.toml (root)[workspace]members = [    "core",    "cli",    "web-api",    "utils"]

[workspace.dependencies]serde = { version = "1.0", features = ["derive"] }tokio = { version = "1.37", features = ["full"] }


Here we have defined four crates: core, cli, web-api, and utils. The key point is [workspace.dependencies]. This allows you to specify common dependencies for all workspace crates. Each crate can reference them without duplicating versions.



Directory structure



A typical monorepo structure in Rust looks like this:



my-monorepo/├── Cargo.toml          # root workspace├── Cargo.lock          # common lock file├── core/│   ├── Cargo.toml│   └── src/│       └── lib.rs├── cli/│   ├── Cargo.toml│   └── src/│       └── main.rs├── web-api/│   ├── Cargo.toml│   └── src/│       └── main.rs├── utils/│   ├── Cargo.toml│   └── src/│       └── lib.rs└── README.md


Each subdirectory is a separate crate with its own Cargo.toml. Crates can be either libraries (lib.rs) or executable applications (main.rs).



Dependency management and code reuse



Internal dependencies



Crates within a workspace can reference each other via local paths. For example, cli can depend on core and utils:



# cli/Cargo.toml[package]name = "cli"version = "0.1.0"edition = "2021"

[dependencies]core = { path = "../core" }utils = { path = "../utils" }serde = { workspace = true }tokio = { workspace = true }


Note the syntax { workspace = true } — it allows you to use common dependencies declared in the root Cargo.toml. This ensures that all crates use the same version of the library.



Example: shared core library



Let's create a simple core library that will contain business logic:



// core/src/lib.rsuse serde::{Serialize, Deserialize};

#[derive(Debug, Clone, Serialize, Deserialize)]pub struct User { pub id: u64, pub name: String, pub email: String,}

pub fn create_user(name: String, email: String) -> User { User { id: rand::random(), name, email, }}

pub fn greet(user: &User) -> String { format!("Hello, {}!", user.name)}


Now the cli crate can use this library:



// cli/src/main.rsuse core::{create_user, greet};

fn main() { let user = create

Blogs

Book Recommendations