Rust's Module and Crate System
Rust code is organized at three nested levels that get conflated constantly because they share vocabulary: modules, which form a namespace-and-privacy tree inside a crate; crates, which are the actual unit the compiler compiles; and packages (or workspaces, for several packages together), which is what Cargo manages. Confusing these - treating "module" and "crate" as synonyms, for instance - is the single most common source of confusion for people organizing their first non-trivial Rust project.
This page is the conceptual anchor for the modules-and-crates section. Modules Basics walks through mod, file layout, and use syntax hands-on; this page explains what each level actually is to the compiler, how Rust's privacy model differs from "public vs. private" in most other languages, and how the workspace mental model scales that same structure across a multi-crate project.
Summary
- Modules form a tree that controls namespacing and visibility within a crate; crates are the compiler's actual unit of compilation; packages (and workspaces of packages) are what Cargo manages and versions.
- Insight: Large codebases need a way to hide implementation details, group related code, and split compilation into independently buildable units - and Rust gives each of those three needs a distinct, purpose-built mechanism instead of one overloaded concept.
- Key Concepts: module tree, privacy (
pub,pub(crate),pub(super)), crate root, package vs. crate, workspace. - When to Use: Modules to organize and encapsulate code within a crate; a new crate boundary when code needs independent compilation, versioning, or a
dyn-free API surface; a workspace when several crates in one repo should share a lockfile and dependency versions. - Limitations/Trade-offs: More crates mean cleaner boundaries but more inter-crate API surface to design and version; deeply nested modules improve organization but add path verbosity that
useonly partially offsets. - Related Topics: ownership and borrowing (privacy protects invariants the same way ownership protects aliasing), trait objects and public API design, Cargo dependency resolution.
Foundations
Every crate has exactly one module tree, rooted at its crate root - src/lib.rs for a library crate, src/main.rs for a binary. Every other module in the crate is a node in that tree, declared with mod name; (loading name.rs or name/mod.rs) or mod name { ... } (declared inline). The tree exists to do two things: give every item a fully-qualified path (crate::api::routes::health), and act as the boundary at which privacy is enforced.
Privacy in Rust is stricter, by default, than in most languages you may have used before: every item - functions, structs, struct fields, enum variants, impl blocks - is private unless explicitly marked otherwise, and "private" means visible only within the module that defines it and that module's descendants. This is the opposite default from languages where "public unless marked private" is the norm, and it means a fresh Rust module, out of the box, is a strong encapsulation boundary rather than an open one.
pub is the most permissive marker, exposing an item to anything that can see the module path leading to it (including, ultimately, other crates, if the path is itself re-exported publicly). Narrower markers exist for narrower needs: pub(crate) exposes an item anywhere within the current crate but nowhere outside it - the idiomatic choice for "shared internal API, not part of our public contract." pub(super) exposes an item to the parent module only. pub(in some::path) restricts visibility to an arbitrary, named subtree. This graduated system is what lets a crate have rich internal structure - helper modules, internal utilities, implementation details - without any of it leaking into the crate's actual public API by accident.
Mechanics & Interactions
It helps to think of the three levels as answering three different questions:
module -> "What can see this item?" (namespace + privacy, inside one crate)
crate -> "What does the compiler compile as one unit?" (one root, one module tree)
package -> "What does Cargo build, version, and publish?" (one Cargo.toml, one or more crates)
A crate is the actual thing rustc compiles: one crate root, its entire module tree, compiled together into one library (.rlib) or binary. Crate boundaries matter for real, technical reasons beyond organization - generic code and trait implementations are monomorphized per crate, incremental compilation caches per crate, and the orphan rule (which restricts implementing a foreign trait for a foreign type) is defined in terms of crate boundaries specifically.
A package is what a single Cargo.toml describes: a name, a version, dependencies, and up to one library crate plus any number of binary, example, test, and benchmark crates. This is the layer where "crate" and "package" get conflated most often, because the common case - one Cargo.toml, one src/lib.rs or src/main.rs - makes package and crate look like the same thing. They diverge as soon as a package defines both a library crate (src/lib.rs) and one or more binaries (src/bin/*.rs, or src/main.rs alongside the library): that's one package containing multiple crates, sharing dependencies but each compiled and monomorphized separately.
# One package, two crates: a library plus a CLI binary that uses it.
[package]
name = "toolkit"
version = "0.1.0"
[lib]
name = "toolkit" # crate 1: the library, src/lib.rs
[[bin]]
name = "toolkit-cli" # crate 2: the binary, src/bin/cli.rs
path = "src/bin/cli.rs"A workspace is the outermost layer: several packages, listed under one root [workspace] table, sharing a single Cargo.lock so every crate in the workspace resolves the same version of every shared dependency. Crucially, a workspace does not change privacy or compilation boundaries at all - each member package still compiles its own crate(s) independently, with its own module tree and its own pub surface facing the others. What a workspace changes is purely a Cargo-level concern: dependency version unification, one shared lockfile, and the ability to run cargo build --workspace or cargo test -p some-crate across the whole set from one root.
Advanced Considerations & Applications
Choosing module depth versus crate boundaries versus workspace members is a real architectural decision, not just a stylistic one, because each level enforces a different strength of separation.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| More modules, one crate | Cheapest to introduce; refactor-friendly; privacy still enforced at module edges | No independent compilation, versioning, or dependency isolation | Organizing a single library or app's internal structure |
| A new crate (same workspace) | Independent compilation unit; forces a deliberate public API surface via pub | More ceremony (own Cargo.toml); inter-crate API changes ripple further | Code that should be reusable, separately tested, or compiled once and reused |
| A new workspace member (separate package) | Independent versioning/publishing if needed later; shared lockfile keeps deps aligned | Most setup overhead; requires thinking about the crate as a product, not just a folder | CLI + library split, plugin crates, code meant to be published separately someday |
The privacy model interacts with crate boundaries in a way that surprises people coming from languages where "public" is a single, flat concept: an item can be pub all the way up its own module tree and still be completely invisible outside the crate, because nothing re-exports it through the crate root. Conversely, pub use lets a crate re-export a deeply-nested item at a shallow, convenient path - this is how a crate presents a clean "prelude"-style public API while keeping its actual internal module structure free to be reorganized without breaking that public surface, as long as the re-export path stays stable. Designing that public surface deliberately - deciding what's pub, what's pub(crate), and what's re-exported at the crate root - is effectively designing your crate's semver contract, since any publicly reachable item is something downstream code can now depend on.
The orphan rule is the sharpest example of crate boundaries mattering beyond organization: you can implement a trait you define for any type, and you can implement a foreign trait for a type you define, but you generally cannot implement a foreign trait for a foreign type - that specific combination is blocked at the crate-boundary level to prevent two crates from defining conflicting implementations of the same trait for the same type. No amount of module nesting changes this; it is fundamentally a crate-level rule, which is one of the clearest signals that "module" and "crate" are not interchangeable concepts even when a small project's structure makes them look that way.
Common Misconceptions
- "Modules and crates are basically the same organizational unit" - A module is a namespace-and-privacy node inside a crate's tree; a crate is the actual thing the compiler compiles as one unit. A crate can (and usually does) contain many modules, but a module is never itself independently compiled.
- "A package always means one crate" - The common case (one
Cargo.toml, onelib.rs) makes this look true, but a package can define a library crate plus several binary crates simultaneously, each compiled separately from the sameCargo.toml. - "
pubon a struct field or item automatically makes it visible outside the crate" -pubmakes an item as visible as the path leading to it allows; if nothing re-exports that path at or above the crate root, the item stays effectively private to external crates even though it's markedpub. - "Everything not marked
pubis visible within the same file" - Privacy is scoped to the module, not the file; a struct declared private in one module is invisible even to sibling code in a different module within the same file, unless that code is nested inside the same module. - "A workspace merges its member crates into one compilation unit" - It doesn't; each workspace member still compiles as its own crate with its own module tree and privacy boundary. A workspace only unifies dependency resolution (one shared
Cargo.lock) and build tooling convenience.
FAQs
What's the actual difference between a module, a crate, and a package?
A module is a namespace-and-privacy node inside a crate's tree. A crate is the unit the compiler actually compiles - one crate root, one module tree. A package is what one Cargo.toml describes, and can contain a library crate plus multiple binary crates at once.
Why is everything private by default in Rust, instead of public by default?
Because encapsulation is the safer default for maintaining invariants - a struct's fields being private by default forces external code through constructor functions and methods that can enforce validity, rather than allowing arbitrary direct mutation from anywhere that can see the type.
What does `pub(crate)` actually buy you over plain `pub`?
It exposes an item everywhere inside the current crate but nowhere outside it, which is the idiomatic way to mark something as shared internal API - usable across your own modules - without accidentally making it part of the crate's external, semver-guaranteed public surface.
Can an item be `pub` and still be invisible to other crates?
Yes - pub only makes an item as visible as the path leading to it. If that path lives inside a private module that nothing re-exports up to the crate root, external crates have no way to reach it even though the item itself is marked pub.
How does `mod name;` decide which file to load?
It looks for name.rs in the current module's directory, or name/mod.rs (or, in newer file layouts, a name/ directory containing further mod declarations without a mod.rs). The module's declared name and its file path are connected by this convention, not by anything inside the file itself.
Is a workspace's `Cargo.lock` shared across all member crates?
Yes - that's the primary thing a workspace changes. All member packages resolve shared dependencies to the same versions via one lockfile at the workspace root, which avoids version drift between crates that live in the same repository.
Does putting two crates in a workspace change how they compile?
No - each workspace member still compiles as its own independent crate with its own module tree, its own privacy boundary, and its own incremental compilation unit. The workspace only affects dependency resolution and build/test tooling convenience (like cargo test --workspace), not compilation boundaries.
Why would I split code into a separate crate instead of just a new module?
A new crate forces a deliberate, pub-defined API surface between it and its consumers, gives it independent compilation (useful for incremental build times), and makes it independently testable, versionable, and potentially publishable - none of which a module boundary provides on its own.
What is `pub use`, and why do crates use it?
It re-exports an item at a new path, letting a crate present a clean, shallow public API (often called a "prelude") at its root while keeping the actual implementation organized in deeper, private-by-default internal modules. Consumers depend on the re-exported path, which stays stable even if the internal module structure is reorganized.
What is the orphan rule, and why is it tied to crates specifically?
It's the restriction that you generally can't implement a foreign trait for a foreign type - only for types or traits your own crate defines. It's enforced at the crate boundary because its purpose is preventing two different crates from each defining a conflicting implementation of the same trait for the same type, which module nesting has no bearing on.
Can a single package produce more than one compiled binary?
Yes - a package can define multiple binary crates via [[bin]] entries or files under src/bin/, each compiled as a separate crate, all sharing the same Cargo.toml, dependencies, and (often) an internal library crate they both depend on.
How do private helper modules coexist with a small public API in the same crate?
By nesting the helpers inside modules that are private (or pub(crate)) by default, and only marking the small set of items that form the intended API as pub, optionally re-exported via pub use at the crate root. The module tree can be arbitrarily deep and reorganized freely as long as the public surface's paths stay stable.
Related
- Modules Basics - hands-on
mod, file layout, andusesyntax. - Visibility & Privacy -
pub,pub(crate),pub(super), and encapsulation patterns in depth. - Crates & Packages - the package/crate distinction with concrete
Cargo.tomlexamples. - Designing a Public API - deliberately shaping a crate's
pubsurface as a semver contract. - Workspaces - multi-crate repos, shared dependencies, and workspace-level Cargo commands.
- Cargo Workspaces - the Cargo-tooling side of workspace configuration.
Stack versions: This page was written for Rust 1.97.0 (edition 2024).