How does the module system work

The Rust module system uses mod, pub, and use to organize code into namespaces and control visibility.

When main.rs becomes a swamp

You're building a text-based adventure game. Your main.rs has grown to four hundred lines. Player stats, inventory management, combat math, and dungeon generation are all tangled together. You try to find the function that calculates damage and scroll past three unrelated loops just to realize you named it calc instead of calculate_damage. You need structure. You need to split this file.

Rust's module system solves this. It lets you group code into namespaces, split logic across files, and control exactly which parts of your code are visible to the rest of the world.

Modules are namespaces with boundaries

A module groups related code. It creates a scope where names don't clash with the rest of your project. More importantly, a module enforces visibility. Items inside a module are private by default. Code outside the module cannot touch them unless you explicitly mark them as public.

Picture a department in a company. The department has internal meetings, private wikis, and draft documents. Other departments can only interact through the public interface. They can request a report or submit a ticket. They cannot walk into the break room and read the draft documents. Rust's module system works the same way. The compiler enforces these boundaries. If you try to access a private item, the code does not compile.

This design keeps your codebase maintainable. Private items are implementation details. You can rename them, change their signature, or delete them without breaking code that depends on your module. Public items form your API. Changing a public item requires care because other code relies on it.

Start with private. Add pub only when code outside the module needs access.

The basics: mod, pub, and paths

The mod keyword declares a module. You can define the module inline or point to a file. Items inside the module use pub to become visible outside. You access items using paths.

/// A module containing math utilities.
/// Items are private by default.
mod math_utils {
    /// Adds two integers.
    /// Marked `pub` so external code can call it.
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }

    /// Subtracts two integers.
    /// No `pub`. Only code inside `math_utils` can use this.
    fn subtract(a: i32, b: i32) -> i32 {
        a - b
    }
}

fn main() {
    // Access the public function via the full path.
    let sum = math_utils::add(2, 3);

    // This line fails to compile.
    // `subtract` is private to `math_utils`.
    // let diff = math_utils::subtract(5, 2);
}

The compiler rejects access to private items with E0603 (module or its content is private). The error points to the item and tells you it is private. Add pub to the item or the parent module to fix it.

You can bring paths into scope with use. This lets you refer to an item by a shorter name.

mod math_utils {
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }
}

// Bring `add` into scope.
// Now you can call `add()` directly.
use math_utils::add;

fn main() {
    let result = add(10, 20);
}

Convention aside: The community prefers use statements at the top of the file. cargo fmt groups and sorts imports automatically. Don't argue style; argue logic.

Splitting code across files

Inline modules work for small examples. Real projects split code across files. When you write mod foo; with a semicolon, Rust looks for a file. It checks foo.rs or foo/mod.rs in the same directory.

// src/main.rs
// Declare the module.
// Rust looks for `src/math_utils.rs` or `src/math_utils/mod.rs`.
mod math_utils;

// Bring `add` into scope.
use math_utils::add;

fn main() {
    let result = add(10, 20);
    println!("Result: {}", result);
}
// src/math_utils.rs
/// Adds two integers.
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

/// Subtracts two integers.
/// Private helper.
fn subtract(a: i32, b: i32) -> i32 {
    a - b
}

Convention aside: The community prefers mod foo; to point to foo.rs. You'll see mod.rs in older codebases. It works, but foo.rs is cleaner for single-file modules. Use mod.rs only when a module has submodules and you want a clear entry point.

Let the file system mirror your module tree. It keeps navigation predictable.

Visibility isn't just public or private

Visibility has granularity. pub exposes an item to the entire world. pub(crate) exposes an item only within the current crate. pub(super) exposes an item only to the parent module. pub(self) is the same as private.

This granularity matters for library crates. You often want to share helpers between modules without exposing them to users of your library.

// src/lib.rs
mod internal {
    /// Configuration struct.
    /// Visible only within this crate.
    /// External crates cannot access this.
    pub(crate) struct Config {
        pub(crate) host: String,
        port: u16,
    }

    /// Helper function.
    /// Visible only to the parent module.
    pub(super) fn parse_host(input: &str) -> String {
        input.trim().to_string()
    }
}

/// Public API.
/// External crates can use this.
pub fn create_config(host: &str) -> internal::Config {
    internal::Config {
        host: internal::parse_host(host),
        port: 8080,
    }
}

Use pub(crate) for library internals. It keeps your public API clean while allowing modules to share helpers.

Path resolution and imports

Paths resolve from the crate root. crate:: starts at the root of your crate. super:: refers to the parent module. Relative paths start from the current module.

Rust resolves paths consistently. If you are in a::b::c, use d::e fails because d is not a sibling of c. You need use crate::d::e or use super::super::d::e.

The compiler rejects unresolved paths with E0432 (unresolved import). The error tells you the path could not be found. Check your path starts with crate:: or super:: correctly.

You can rename imports to avoid name clashes.

// Rename `HashMap` to `Map`.
use std::collections::HashMap as Map;

fn main() {
    let mut map = Map::new();
    map.insert("key", "value");
}

Always resolve paths from the crate root. Relative paths break when you move files.

Re-exporting to flatten your API

Sometimes you want to hide internal structure but expose an item. Use pub use to re-export. The caller sees the item as coming from your module, not the original location.

mod internal {
    pub fn secret_function() {}
}

// Re-export `secret_function` as public.
// Callers use `my_module::secret_function()`.
// They don't see `internal`.
pub use internal::secret_function;

Re-exporting flattens your API. Callers don't need to know about your internal module hierarchy. They see a clean interface.

Re-export to hide implementation details. Callers should see a flat interface, not your directory structure.

Decision matrix

Use mod to group related code into a namespace and split your code across files. Use mod when a file grows beyond a few hundred lines or when you have distinct logical units like network, database, and ui.

Use pub to expose an item to code outside its module. Use pub for the API surface of your module. Keep implementation details private.

Use pub(crate) to expose an item within the crate but hide it from external crates. Use pub(crate) for library internals that multiple modules need to share.

Use pub(super) to expose an item only to the parent module. Use pub(super) when a submodule needs to share a helper with its parent but nothing else.

Use use to bring a path into scope so you can refer to an item by a shorter name. Use use when you call an item frequently and the full path is verbose.

Use pub use to re-export an item from a submodule. Use pub use when you want to flatten your public API and hide internal module structure from callers.

Use crate:: in paths to refer to the crate root. Use crate:: for absolute paths to avoid ambiguity.

Use super:: in paths to refer to the parent module. Use super:: when writing relative paths inside a submodule.

Treat pub as a contract. If you mark it public, you own that API forever.

Where to go next