Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
125: Workspaces for Multi-Crate Projects
Why should I bother with a workspace instead of just one big crate?
I've seen a lot of developers start with a single crate and then hit a wall when the project grows. Imagine you're building a complex simulation tool—let's call it quantum_sim. At first, it's just a few files. But eventually, you realize you have a massive amount of physics logic that could be useful for other projects, and a CLI wrapper that's mostly just handling user input and printing to the console.
If you keep them in one crate, every time you change a tiny bit of formatting in your CLI, Cargo might have to do more work than necessary. More importantly, your boundaries get blurry. When you separate the physics engine into its own crate within a workspace, you're forcing yourself to define a clean public API. It also speeds up compilation in larger projects because Cargo can better parallelize the build process across different crates.
How do I actually set up the folder structure and the root config?
The most important thing to understand is the "virtual manifest." This is a Cargo.toml file at the root of your project that doesn't define a package itself, but instead manages other packages. Here is how I'd structure our quantum_sim project:
quantum_sim/
├── Cargo.toml
├── sim_core/
│ ├── Cargo.toml
│ └── src/lib.rs
└── sim_cli/
├── Cargo.toml
└── src/main.rs
Your root Cargo.toml should look like this:
[workspace]
members = [
"sim_core",
"sim_cli",
]
That's it. Now, when you run cargo build from the root, Cargo looks at those members and builds everything. One huge win here is that all these crates now share a single Cargo.lock file. This ensures that if both sim_core and sim_cli use serde, they're using the exact same version, preventing weird dependency conflicts and reducing disk space.
How do the crates within the workspace talk to each other?
Since sim_cli needs the logic inside sim_core, you have to declare it as a dependency. But instead of pointing to a version on crates.io, you point to the local path. In sim_cli/Cargo.toml, you'd add:
[dependencies]
sim_core = { path = "../sim_core" }
I usually recommend this approach over relative paths if you're planning on publishing, but for internal workspace organization, it's the standard. Now you can just use use sim_core::some_function(); inside your CLI code as if it were any other external library.
Can I avoid repeating dependency versions in every single crate?
If you have ten crates in a workspace and all of them use tokio, updating the version in ten different Cargo.toml files is a nightmare. I used to do this manually until Rust introduced workspace inheritance. Now, you can define the version once in the root manifest.
In your root Cargo.toml:
[workspace]
members = ["sim_core", "sim_cli"]
[workspace.dependencies]
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
Then, in sim_core/Cargo.toml or sim_cli/Cargo.toml, you just do this:
[dependencies]
tokio = { workspace = true }
serde = { workspace = true }
This is a lifesaver. It keeps your project DRY (Don't Repeat Yourself) and makes upgrading dependencies a one-line change at the root.
📋 Practical Task
Refactoring a Payment Processor into a Workspace
You have a monolithic project that handles payment processing. Your goal is to split it into a workspace to separate the business logic from the API layer. Perform the following steps:
- Create a root directory named
payment_systemwith aCargo.tomldefining a workspace with two members:payment_core(a library) andpayment_api(a binary). - In the root
Cargo.toml, define a shared dependency forchrono = "0.4"using the[workspace.dependencies]table. - In
payment_core/src/lib.rs, create a functionprocess_payment(amount: f64)that returns a string. Ensurepayment_coreuses thechronodependency via workspace inheritance. - In
payment_api/src/main.rs, call theprocess_paymentfunction from thepayment_corecrate. - Configure
payment_api/Cargo.tomlto depend onpayment_coreusing a path dependency and onchronousing workspace inheritance.
Verify your work by running cargo run -p payment_api from the root directory and seeing the payment result printed to the console.
There are no comments for now.