The Module System
Rust's Module System
Rust's module system is a unified mechanism for organizing code, managing namespaces, and controlling visibility. Compared to other languages' module systems, its distinguishing features are private by default and explicit visibility level control.
Module Basics
The mod Keyword
Modules are defined with the mod keyword:
// Defining a module within the same file
mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
fn internal_helper() -> i32 {
// Private: not accessible from outside this module
42
}
}
fn main() {
let result = math::add(1, 2);
// math::internal_helper(); // Compile error: private
}
File System and Modules
Rust's module system maps to the file system's directory structure:
src/
├── main.rs (crate root)
├── math.rs (math module)
└── network/
├── mod.rs (network module)
└── http.rs (network::http submodule)
// src/main.rs
mod math; // Loads src/math.rs
mod network; // Loads src/network/mod.rs
fn main() {
math::add(1, 2);
network::http::get("https://example.com");
}
Since Rust 2018 edition, you can also represent modules by filename instead of mod.rs:
src/
├── main.rs
├── math.rs
└── network.rs (network module)
└── network/
└── http.rs (network::http submodule)
Private by Default
The most important design decision in Rust's module system is that all items are private by default.
mod my_module {
struct InternalData {
value: i32,
}
fn internal_function() -> i32 {
42
}
pub fn public_function() -> i32 {
// Accessible from within the same module
internal_function()
}
}
In Java, the default is package-private. In Python, the convention is to use underscore prefixes to indicate private, but it's not enforced. In JavaScript (ESM), the default is module-private but you export to make things public.
Rust's approach is "only what you explicitly mark with pub is public" -- a design faithful to the principle of least privilege. This means:
- Internal implementation changes don't break API compatibility: Anything without
pubis guaranteed not to be used externally - Prevents accidental exposure: No risk of accidentally making internal functions public
- Provides cues when reading code: Whether
pubis present immediately tells you if an item is a public API or an internal implementation
Fine-Grained Visibility Control
Rust allows fine-grained control of visibility by adding modifiers to pub:
mod outer {
pub mod inner {
// Accessible from anywhere
pub fn fully_public() {}
// Accessible only from the parent module (outer)
pub(super) fn parent_only() {}
// Accessible only within the same crate
pub(crate) fn crate_only() {}
// Accessible only from a specific path
pub(in crate::outer) fn specific_path() {}
// Default: only within this module
fn private() {}
}
fn test() {
inner::fully_public(); // OK
inner::parent_only(); // OK: this is the parent module
inner::crate_only(); // OK: same crate
inner::specific_path(); // OK: we're in crate::outer
// inner::private(); // NG: private
}
}
| Visibility | Accessible From |
|---|---|
pub |
Anywhere (including external crates) |
pub(crate) |
Within the same crate |
pub(super) |
The parent module |
pub(in path) |
Under the specified path |
| (none) | Within the same module only |
This graduated visibility control is especially useful in large projects. Use pub(crate) when "you don't want to expose it to external crates but want it freely available within the crate," and pub(super) when "it's a submodule's implementation detail but the parent module needs access."
use: Simplifying Paths
The use keyword introduces a module path into the current scope:
// Full path access
std::collections::HashMap::new();
// Introduced with use
use std::collections::HashMap;
HashMap::new();
Flexible use Syntax
// Import multiple items at once
use std::collections::{HashMap, HashSet, BTreeMap};
// Import the module itself
use std::collections;
let map = collections::HashMap::new();
// Merge nested paths
use std::io::{self, Read, Write};
// The above is equivalent to:
// use std::io;
// use std::io::Read;
// use std::io::Write;
// Glob import (commonly used in tests)
use std::collections::*;
// Alias
use std::collections::HashMap as Map;
use Placement: Top-Level and Local
use can be written not only at the top of files but also inside functions and blocks:
fn process() {
use std::collections::HashMap; // Only valid within this function
let mut map = HashMap::new();
map.insert("key", "value");
}
This flexibility lets you minimize namespace pollution while importing only what you need, where you need it.
Conventional use Style
The Rust community has conventions for how to write use:
// 1. Standard library
use std::collections::HashMap;
use std::io::{self, Write};
// 2. External crates
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
// 3. Items from within the crate
use crate::config::Settings;
use crate::models::User;
cargo fmt automatically organizes this ordering for you.
Path Origins
Rust module paths have three starting points:
mod a {
pub mod b {
pub fn func() {}
}
pub fn example() {
// Absolute path: from crate root
crate::a::b::func();
// Relative path: from current module
b::func();
// self: explicitly the current module
self::b::func();
// super: parent module
// super::other_module::func();
}
}
| Origin | Meaning |
|---|---|
crate:: |
Absolute path from the crate root |
self:: |
The current module |
super:: |
The parent module |
| (none) | Relative path from the current scope |
Re-export: pub use
pub use re-exports an item:
// src/lib.rs
mod internal {
pub mod deeply {
pub mod nested {
pub struct ImportantType;
}
}
}
// Hide internal structure while exposing via a convenient path
pub use internal::deeply::nested::ImportantType;
// Externally accessible as my_crate::ImportantType
// The internal module structure is hidden
pub use is an important tool for public API design. Even if you refactor internal module structure, the external API remains unchanged as long as you maintain the pub use paths.
The Relationship Between Crates and Modules
Rust's compilation unit is the crate:
- Binary crate: An executable program with
fn main() - Library crate: A library used by other crates
my_project/
├── Cargo.toml
├── src/
│ ├── main.rs (binary crate root)
│ └── lib.rs (library crate root)
A single package (the unit defined by Cargo.toml) can contain one library crate and multiple binary crates.
Summary
Design principles of Rust's module system:
- Private by default: All items are private by default. Explicitly publish with
pub - Graduated visibility levels: Fine-grained control with
pub,pub(crate),pub(super),pub(in path) - File system correspondence: Module structure maps to directory structure, making physical code layout match logical structure
- Flexible use syntax: Nested imports, aliases, local-scope use
- Re-export:
pub useseparates the API surface from internal structure
This design enforces the software engineering principle of "hiding internal implementation and intentionally controlling the public API" at the language level. In large projects, module boundaries naturally become API design boundaries, providing a foundation for maintainability and ease of refactoring.