Modules, crates, and Cargo
In one sentence
Section titled “In one sentence”Cargo is Maven and Gradle in one tool: Cargo.toml declares the package and its dependencies, a crate is the unit that the compiler builds (like a JAR), and modules inside a crate organize the code and control privacy.
The Java you know
Section titled “The Java you know”Java
<project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>invoice-tool</artifactId> <version>0.1.0</version>
<properties> <maven.compiler.release>21</maven.compiler.release> </properties>
<dependencies> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.17.2</version> <!-- one exact version --> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.10.3</version> <scope>test</scope> <!-- like [dev-dependencies] --> </dependency> </dependencies></project>Rust
[package]name = "cargo-demo"version = "0.1.0"edition = "2024" # the language edition, chosen per craterust-version = "1.99" # the oldest compiler that this crate supportsdescription = "A small invoice tool. It shows the parts of a Cargo package."license = "MIT"publish = false
[dependencies]serde = { version = "1.0.229", features = ["derive"] } # "1.0.229" means ">=1.0.229, <2.0.0"serde_json = { version = "1.0.154", optional = true } # only built when a feature asks for itthiserror = "2.0.21"
[dev-dependencies] # only for tests and examplesserde_json = "1.0.154"
[features]default = ["json"] # on unless the user turns them offjson = ["dep:serde_json"] # feature "json" turns on the optional crateThe manifest is shorter, and it has no group ID: crate names on crates.io are global. The examples on this site use a real package with this manifest, examples/cargo-demo/, which CI builds and tests.
The Rust you will see
Section titled “The Rust you will see”Crates, packages, workspaces
Section titled “Crates, packages, workspaces”| Rust | Java | Meaning |
|---|---|---|
| crate | a JAR, or an assembly in .NET | what the compiler builds in one step: a library or a binary |
| package | a Maven module | one Cargo.toml, with one library crate and/or one or more binary crates |
| workspace | a multi-module Maven project | several packages with one shared Cargo.lock and target/ folder |
| crates.io | Maven Central | the public registry |
| docs.rs | javadoc.io | the documentation of every published crate |
A package with src/main.rs builds a binary. A package with src/lib.rs builds a library. It can have both.
Version requirements
Section titled “Version requirements”serde = "1.0.229" does not mean “exactly 1.0.229”. It is a caret requirement: any version from 1.0.229 up to, but not including, 2.0.0. Cargo picks the newest version that fits, and writes it into Cargo.lock. This is semantic versioning: Cargo assumes that a minor version does not break the API.
| Requirement | Accepts |
|---|---|
"1.0.229" or "^1.0.229" |
>= 1.0.229, < 2.0.0 |
"0.15" |
>= 0.15.0, < 0.16.0 (for 0.x, the minor version is the breaking one) |
"~1.2.3" |
>= 1.2.3, < 1.3.0 |
"=1.2.3" |
exactly 1.2.3 |
"*" |
any version. crates.io does not accept published crates with this |
Cargo.lock
Section titled “Cargo.lock”Cargo.lock records the exact version of every dependency, also the indirect ones. cargo new adds it to version control by default, for applications and libraries. The Cargo documentation says that whether to commit it depends on the needs of the project. For an application, commit it: it makes builds repeatable. Note that a library’s Cargo.lock does not affect the users of the library. Only its Cargo.toml does.
Features
Section titled “Features”A feature is a named switch that turns on optional code or optional dependencies. In the manifest above, json turns on the optional serde_json dependency, and default turns json on unless the user asks for no default features. In the code, #[cfg(feature = "json")] marks the parts that need it:
mod billing; // the module's code is in `src/billing.rs`
use billing::{Invoice, Line, VAT_PER_MILLE};
fn main() { let invoice = Invoice { number: "2026-001".to_string(), lines: vec![Line { item: "Book".to_string(), cents: 2_000, }], }; println!("VAT: {VAT_PER_MILLE} per mille"); println!("total with VAT: {}", invoice.total_with_vat_cents()); #[cfg(feature = "json")] // compiled only when the "json" feature is on println!( "{}", billing::to_json(&invoice).expect("an invoice is always valid JSON") );}Many crates put large parts behind features. tokio = { version = "1", features = ["full"] } turns on all of them. In review, ask if the code needs all the features that it turns on: each one adds compile time and code.
How to read it
Section titled “How to read it”Modules and files
Section titled “Modules and files”A module is a namespace with privacy, like a Java package. Unlike Java, a module is declared in code with mod, and the file layout follows the declarations:
| File | Module path | Declared by |
|---|---|---|
src/main.rs |
crate (the root) |
(the crate root) |
src/billing.rs |
crate::billing |
mod billing; in main.rs |
src/billing/tax.rs |
crate::billing::tax |
mod tax; in billing.rs |
The older layout src/billing/mod.rs instead of src/billing.rs still works, and you will see it in many crates. Both mean the same.
mod tax; // private: code outside `billing` cannot use `billing::tax`
pub use tax::VAT_PER_MILLE; // a re-export: callers write `billing::VAT_PER_MILLE`
use serde::Serialize;
#[derive(Debug, Serialize)] // `Serialize` comes from the `serde` crate, with its "derive" featurepub struct Line { pub item: String, pub cents: u64,}
#[derive(Debug, Serialize)]pub struct Invoice { pub number: String, pub lines: Vec<Line>,}
impl Invoice { pub fn total_with_vat_cents(&self) -> u64 { let net: u64 = self.lines.iter().map(|l| l.cents).sum(); net + tax::vat_cents(net) // a private item of a child module, used by its parent }}
#[cfg(feature = "json")]pub fn to_json(invoice: &Invoice) -> Result<String, serde_json::Error> { serde_json::to_string(invoice)}/// The Swiss standard VAT rate, in parts per thousand (8.1 %).pub const VAT_PER_MILLE: u64 = 81;
/// `pub(super)`: visible to the parent module `billing`, and nowhere else.pub(super) fn vat_cents(net_cents: u64) -> u64 { net_cents * VAT_PER_MILLE / 1_000}mod tax;is private, so onlybillingcan usebilling::tax.pub use tax::VAT_PER_MILLE;is a re-export: the constant is defined intax, but callers see it asbilling::VAT_PER_MILLE. Libraries usepub useto give a short public API while the files stay organized. Java has no equivalent.pub(super)makesvat_centsvisible to the parent module only.
Paths: crate, super, self
Section titled “Paths: crate, super, self”pub mod shop { pub mod catalog { pub fn price_cents(item: &str) -> Option<u64> { match item { "book" => Some(2_000), "pen" => Some(150), _ => None, } } }
pub mod cart { use super::catalog; // `super`: the parent module, `shop`
pub fn total_cents(items: &[&str]) -> u64 { items.iter().filter_map(|i| catalog::price_cents(i)).sum() }
pub fn total_text(items: &[&str]) -> String { self::format(total_cents(items)) // `self`: this module, `cart` }
fn format(cents: u64) -> String { crate::modules_crates_and_cargo::money::format_cents(cents) // `crate`: the crate root } }}
pub mod money { pub fn format_cents(cents: u64) -> String { format!("{}.{:02}", cents / 100, cents % 100) }}| Path start | Means | Java |
|---|---|---|
crate:: |
the root of this crate | the fully qualified name in your project |
super:: |
the parent module | (none) |
self:: |
this module | (none) |
serde:: |
an external crate, from Cargo.toml |
an import from a dependency |
std:: |
the standard library | java.* |
use imports a name, like Java’s import. use super::catalog; then catalog::price_cents(...) is the common style for functions: import the module, and call through it. For types, import the type itself: use std::collections::HashMap;.
Editions
Section titled “Editions”An edition is a version of the language that a crate chooses in Cargo.toml: edition = "2024". The editions are 2015, 2018, 2021, and 2024. An edition changes syntax and some defaults inside one crate. Crates of different editions work together in one program. Some examples:
- 2018:
extern crateis no longer needed, and paths start withcrate::. - 2021: closures capture single fields instead of whole variables.
IntoIteratorfor arrays gives values. - 2024:
genis a reserved keyword,unsafe externblocks are required, andimpl Traitin return types captures all lifetimes in scope by default.
When an agent writes edition = "2018" or no edition at all (which means 2015) in a new crate, ask for 2024. See S27: Outdated idioms (try!, extern crate, lazy_static, old edition).
build.rs
Section titled “build.rs”A file build.rs next to Cargo.toml is a build script: Rust code that Cargo compiles and runs before it builds the crate. It generates code, compiles C libraries, or reads the environment. It is like a Maven plugin that runs in the generate-sources phase. In review, read it as carefully as the crate itself: it runs with your permissions on every build.
What good looks like
Section titled “What good looks like”- Few dependencies, each with a reason. Common, well-maintained crates for common tasks (see the library map in the reference section).
- Version requirements without
*, and a committedCargo.lockfor applications. - Only the features that the code uses.
- Small public API: private modules, and
pub usefor the names that callers need. - The current edition (2024) in new crates.
- A workspace when a project has several packages, like a multi-module Maven build.
Red flags in AI output
Section titled “Red flags in AI output”- Many crates for small tasks:
regexto split on a comma,lazy_staticfor one map, a crate to pad a string. See S28: Too many or unknown dependencies. - Wildcard versions:
chrono = "*". - Crates or APIs that do not exist, or that do not do what the code expects. See S26: Hallucinated crate, API, or feature flag.
features = ["full"]on large crates, or default features that pull in code the program does not use.extern crateand#[macro_use]in new code, or an old edition. See S27: Outdated idioms (try!,extern crate,lazy_static, old edition).pubon every module and item, anduse some::module::*glob imports. See S38:pubon everything.
- S26Severity: HighHallucinated crate, API, or feature flagA dependency, function, or feature that does not exist, or does not do what the code expects.
- S27Severity: LowOutdated idioms (
try!,extern crate,lazy_static, old edition)Old patterns that the current standard library or edition replaces. - S28Severity: MediumToo many or unknown dependenciesCrates added for small tasks, or from unknown authors.
- S38Severity: Low
pubon everythingEvery field and function is public, so the API has no boundary.
Better vs worse
Section titled “Better vs worse”The task: organize two money helpers and an invoice function.
WorseThree levels of
pub mod(utils::helpers::money), everything public, and a glob import that hides where names come from.pub mod utils {pub mod helpers {pub mod money {pub fn format_cents(cents: u64) -> String {format!("{}.{:02}", cents / 100, cents % 100)}pub fn round_to_francs(cents: u64) -> u64 {cents.div_ceil(100) * 100}}}}pub mod invoice {use super::utils::helpers::money::*; // which names does this import?pub fn total_text(cents: u64) -> String {format_cents(round_to_francs(cents))}}Run in Playground (opens the Rust Playground in a new tab) BetterA private
moneymodule, named imports, and onepub usefor the function that callers need.mod money {pub fn format_cents(cents: u64) -> String {format!("{}.{:02}", cents / 100, cents % 100)}pub fn round_to_francs(cents: u64) -> u64 {cents.div_ceil(100) * 100}}pub mod invoice {use super::money::{format_cents, round_to_francs};pub fn total_text(cents: u64) -> String {format_cents(round_to_francs(cents))}}pub use invoice::total_text; // the one function that callers need, at a short pathRun in Playground (opens the Rust Playground in a new tab)
The “worse” layout is a Java habit: a util package with sub-packages, and every class public. In Rust, every pub item in a library is part of its API. Here, callers can use worse::utils::helpers::money::round_to_francs, so it cannot be changed without breaking them. The glob import use ...::money::* makes the reader look up each name.
The “better” version keeps money private, imports names one by one, and re-exports total_text at a short path. Callers write better::total_text.
Check yourself
Section titled “Check yourself”A manifest asks for
regexversion 1.13. Which versions does Cargo accept?Show the answer
Any version from 1.13.0 up to, but not including, 2.0.0. Cargo uses the newest one that fits, and records it in
Cargo.lock.Where is the code of
mod billing;declared insrc/main.rs?Show the answer
In
src/billing.rs, or in the older layoutsrc/billing/mod.rs.What does
pub use tax::VAT_PER_MILLE;inbilling.rsdo?Show the answer
It re-exports the constant: callers can write
billing::VAT_PER_MILLE, even though it is defined in the private modulebilling::tax.An agent added
tokiowith version*and thefullfeature to a command-line tool with no async code. What do you ask?Show the answer
Why tokio is needed at all. If it is needed, ask for a real version requirement instead of
*, and only the features that the code uses.