Skip to Content
Course content

73: Managing Crates and Workspaces

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Think of a large professional kitchen. You don't just have one giant table where everyone is chopping, frying, and plating all at once—that would be absolute chaos. Instead, you have stations. There's a prep station, a grill station, and a pastry station. Each station has its own specific tools and focus, but they all operate within the same kitchen, share the same pantry (dependencies), and are all working toward the same goal: getting the meal out to the customer.

In Rust, a Workspace is that kitchen. The individual Crates are the stations. When your project grows beyond a single binary or a single library, trying to cram everything into one crate makes your compile times crawl and your module tree a nightmare to navigate. By splitting your project into a workspace, you can isolate your logic into smaller, manageable pieces that still feel like a single project.

Defining the Kitchen Perimeter

To start a workspace, you don't use a typical cargo new command for the root. Instead, you create a directory and place a Cargo.toml file in it that tells Rust, "Hey, I'm not a crate myself; I'm a manager for other crates."

[workspace]
members = [
    "core_logic",
    "api_server",
    "cli_tool",
]
resolver = "2"

I usually recommend the resolver = "2" setting because it handles feature unification much more intelligently than the old version. Now, the root directory doesn't have a src folder. It just sits there, orchestrating the members listed in that array.

Connecting the Stations

Let's say we're building a payment processing system. You'd probably have a core_logic crate that handles the actual math and validation, and a cli_tool that lets admins trigger refunds. The cli_tool needs to use the functions inside core_logic.

You don't publish core_logic to crates.io just to use it locally. Instead, you use a path dependency in the cli_tool/Cargo.toml:

[dependencies]
core_logic = { path = "../core_logic" }

This is where the magic happens. When you run cargo build from the root of the workspace, Cargo is smart enough to figure out the dependency graph. It builds core_logic first, then feeds it into the cli_tool. If you only change a line of code in the CLI, Cargo won't bother re-compiling the core logic. This is a huge win for your productivity as the codebase grows.

The Single Source of Truth for Dependencies

One thing that often trips people up is where the Cargo.lock file lives. In a workspace, there is only one Cargo.lock file, and it lives at the root.

I love this because it ensures that if both your api_server and your cli_tool use the serde crate, they are using the exact same version. You'll never run into a bizarre bug where your library expects one version of a dependency but your binary is providing another. It keeps the entire "kitchen" in sync.

Managing Shared Dependencies

If you find yourself adding the same five crates to every single member of your workspace, you can define them in the root Cargo.toml using [workspace.dependencies]. This is like putting a staple ingredient in the main pantry rather than buying a separate bag for every station.

# Root Cargo.toml
[workspace.dependencies]
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }

Then, in your member crates, you just reference them like this:

# cli_tool/Cargo.toml
[dependencies]
tokio = { workspace = true }
serde = { workspace = true }

This makes upgrading a dependency a breeze. Change the version once in the root, and every crate in your workspace is updated instantly. It's a small detail, but it saves you from a lot of tedious manual editing when your project hits 10+ crates.




📋 Practical Task

Exercise: Engineering a Multi-Crate Payment Processor Engine

Your goal is to restructure a theoretical payment system into a proper Rust workspace to improve modularity and compile times.

  1. Create a new directory named payment_system.
  2. Initialize a workspace by creating a Cargo.toml file at the root. Define a workspace with two members: payment_core and payment_cli. Use resolver = "2".
  3. Inside the payment_system folder, create the two crates:
    • cargo new payment_core --lib
    • cargo new payment_cli
  4. In payment_core/src/lib.rs, implement a simple function pub fn process_payment(amount: f64) -> String that returns a string saying "Processed payment of $[amount]".
  5. In the root Cargo.toml, add serde = "1.0" to a [workspace.dependencies] section.
  6. Configure payment_cli/Cargo.toml to depend on:
    • payment_core (via a relative path).
    • serde (via the workspace inheritance).
  7. In payment_cli/src/main.rs, call the process_payment function from the core library and print the result to the console.
  8. Run cargo build from the root directory to ensure the entire workspace compiles successfully.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.