Rust's Module System Explained: Packages, Crates, Modules, and Paths
As a Rust project grows in scale, placing all source code in a single file becomes unmanageable. Rust provides a structured module system designed to handle code organization, encapsulation, and interface exposure. This note breaks down how Rust manages growing codebases using Packages, Crates, Modules, Paths, and Privacy Rules.
1. Project Organization Architecture
When building software, encapsulation is critical. For example, in an authentication system, internal implementation details (such as password hashing or token validation mechanisms) should remain hidden from the rest of the application. The program should only expose a simple interface—such as a public login() function.
To enforce boundaries and maintain order, Rust structures code into a clear operational hierarchy:
2. Packages and Crates
Basic Definitions
- Crate: The smallest unit of code that the Rust compiler considers at a time. Crates come in two flavors:
- Binary Crate: An executable program that compiles into a runnable file. It must contain a
mainfunction. - Library Crate: Reusable code meant to be shared across multiple programs. It does not compile to an executable directly and does not have a
mainfunction.
- Binary Crate: An executable program that compiles into a runnable file. It must contain a
- Package: A collection of one or more crates that provides a set of functionality. A package contains a
Cargo.tomlmanifest file describing how to build those crates.
Cargo File Structure Conventions
When you create a package with cargo new my_project, Cargo establishes the project defaults without requiring explicit crate declarations inside Cargo.toml:
Note: The Crate Root (such as
src/main.rsorsrc/lib.rs) is the entry source file that the Rust compiler reads first. It forms the root module of your crate tree.
Package Composition Rules
3. Designing a Domain Module Tree (The Restaurant Analogy)
Modules let you group related items, control access, and name scope boundaries cleanly.
Real-Life Analogy: Restaurant Operations
To understand modules, consider how a restaurant operates split into two major domains:
- Front of House (
front_of_house): Where customers interact. Includes seating, taking orders, and accepting payments. - Back of House (
back_of_house): Where food is prepared, dishes are washed, and administrative duties occur.
// Defined inside src/lib.rs (The Crate Root)
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
The Inherent Module Tree
The code above automatically forms a tree structure matching how filesystem folders work on your operating system:
4. Navigating the Module Tree with Paths
To invoke an item inside a module tree, you must specify its path using double colons (::) as the path separator.
Absolute vs. Relative Paths
- Absolute Path: Starts from the crate root module using the literal keyword
crate. - Relative Path: Starts from the current module context using an identifier (module name) or keyword (
self,super).
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
// Function defined at the crate root level
pub fn eat_at_restaurant() {
// 1. Absolute Path
crate::front_of_house::hosting::add_to_waitlist();
// 2. Relative Path (eat_at_restaurant and front_of_house are siblings under `crate`)
front_of_house::hosting::add_to_waitlist();
}
5. Encapsulation & Privacy Rules (pub Keyword)
Rust keeps implementation details secret by default.
Privacy Rules Core Rules
- Default State: All items (functions, methods, structs, enums, modules, constants) are private by default.
- Parent Access to Child: Parent modules cannot access private items defined inside their child modules.
- Child Access to Parent: Child modules can access any item defined within their parent modules (and grandparent modules).
- Public Exposure: Marking an item with
pubexposes it to parent modules and external programs.
mod front_of_house {
// Without `pub`, hosting is private to front_of_house's parent scope
pub mod hosting {
// Without `pub`, function is private to hosting's parent scope
pub fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Calling this succeeds because both `hosting` module
// and `add_to_waitlist` function are marked `pub`.
crate::front_of_house::hosting::add_to_waitlist();
}
Compilation Error Warning: Making a module
pubmakes its module structure visible to parents, but does not automatically make its inner items public. You must mark both the container module and the contained itempub.
6. Navigating Parent Context with super
Relative paths can reference parent modules using the super keyword at the start of a path (equivalent to .. in a Unix file system path).
fn serve_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
// Navigate up to parent scope (`crate`) to access serve_order
super::serve_order();
}
fn cook_order() {}
}
7. Privacy Dynamics: Structs vs. Enums
Privacy mechanics behave differently when applied to Structs compared to Enums.
A. Struct Privacy Mechanics
- Marking a struct
pubmakes the struct type public. - Struct fields remain private by default. Each field must be explicitly marked
pubif external access is allowed. - If a struct contains any private fields, it cannot be instantiated directly outside its module using struct literal syntax. It must be created via a public constructor/associated function.
mod back_of_house {
pub struct Breakfast {
pub toast: String, // Public field: Can be read and modified
seasonal_fruit: String, // Private field: Restricted to back_of_house
}
impl Breakfast {
// Public constructor associated function
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
pub fn eat_at_restaurant() {
// Order a meal using the public constructor
let mut meal = back_of_house::Breakfast::summer("Rye");
// We can modify public fields directly
meal.toast = String::from("Wheat");
// Error: seasonal_fruit is private!
// meal.seasonal_fruit = String::from("blueberries");
// Error: Cannot instantiate directly because `seasonal_fruit` field is private!
// let meal2 = back_of_house::Breakfast { toast: String::from("Rye"), seasonal_fruit: String::from("peaches") };
}
B. Enum Privacy Mechanics
- Marking an enum
pubautomatically makes all of its variants public.
mod back_of_house {
pub enum Appetizer {
Soup, // Public automatically
Salad, // Public automatically
}
}
pub fn eat_at_restaurant() {
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}
8. Bringing Paths into Scope with use
Repeatedly writing full module paths can cause code bloat. The use keyword creates a local path shortcut into the active scope (similar to creating a symbolic link in a filesystem).
Idiomatic Import Conventions
Resolving Naming Conflicts: Two Strategies
Strategy 1: Bring Parent Modules into Scope
use std::fmt;
use std::io;
fn function1() -> fmt::Result { Ok(()) }
fn function2() -> io::Result<()> { Ok(()) }
Strategy 2: Disambiguate with the as Keyword
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result { Ok(()) }
fn function2() -> IoResult<()> { Ok(()) }
9. Re-Exporting Names with pub use
When you import a path into scope using use, the imported name is private to the current scope. By combining pub and use, you re-export the item. This exposes the imported path to any code calling your module.
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
// Re-export hosting module for external consumers
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
Why Re-Export? It separates internal code organization from external presentation. You can organize your code into deep module trees internally, while providing external users a clean, flat public API.
10. External Packages, Nested Paths, and Globs
A. Importing External Crates
To use external dependencies:
- Declare the package under
[dependencies]inCargo.toml. - Bring items into scope with
use.
# Cargo.toml
[dependencies]
rand = "0.8.5"
// src/lib.rs
use rand::Rng;
pub fn generate_secret() -> u32 {
rand::thread_rng().gen_range(1..=100)
}
B. Grouping Imports with Nested Paths
To clean up multiple imports from the same library or module, collapse paths into a single line using nested braces {}:
// Un-nested (bloated)
use std::cmp::Ordering;
use std::io;
// Refactored using Nested Paths
use std::{cmp::Ordering, io};
Combining sub-paths and parent modules:
// Un-nested
use std::io;
use std::io::Write;
// Refactored using 'self' nested paths
use std::io::{self, Write};
C. The Glob Operator (*)
To bring all public items defined in a path into scope, use the glob operator (*):
use std::collections::*;
Warning: Use the glob operator sparingly. It pollutes the local namespace and can lead to unexpected identifier conflicts when libraries add new items over time.
11. Extracting Modules into Separate Files
As modules expand, move their implementation into dedicated files.
Workflow Rule Set for Multi-File Modules
- Declare the module in the root file using
mod <module_name>;with a semicolon instead of an inline code block ({ ... }). - The compiler will search for the module's code in:
src/<module_name>.rssrc/<module_name>/mod.rs(older style)
- For sub-modules of
<module_name>, place the sub-module files inside a sub-directory namedsrc/<module_name>/.
Example File Decomposition Walkthrough
1. Crate Root (src/lib.rs)
// Declares front_of_house module; compiler loads content from src/front_of_house.rs
mod front_of_house;
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
2. Parent Module File (src/front_of_house.rs)
// Declares hosting sub-module; compiler loads content from src/front_of_house/hosting.rs
pub mod hosting;
3. Child Module File (src/front_of_house/hosting.rs)
pub fn add_to_waitlist() {}
Final On-Disk Directory Hierarchy
12. Summary and Key Takeaways
- Hierarchical Layout: Rust builds software projects from Packages Crates Modules Items.
- Convention-Over-Configuration:
src/main.rsimplicitly serves as a binary crate root;src/lib.rsimplicitly serves as a library crate root. Additional binaries belong insrc/bin/. - Default Privacy Baseline: Everything is private by default. Child modules can read parent implementations, but parent modules cannot access child implementations without explicit
pubvisibility. - Path Construction: Paths use double colons (
::). Absolute paths start fromcrate::; relative paths can usesuper::to access parent modules. - Struct vs. Enum Privacy: Marking a struct
publeaves its fields private by default. Marking an enumpubexposes all of its variants publicly. - Idiomatic
useScoping: Import parent modules when bringing functions into scope. Import items directly when working with structs, enums, or traits. - API Surface Optimization: Use
pub useto re-export items. This lets you maintain clean internal module trees while exposing simple, user-friendly external APIs. - File System Decomposition: Replace inline module brackets (
mod name { ... }) with semicolons (mod name;) to split modules across individual files and subdirectories.