Every way to organize code
Functions, modules, files, visibility, library and binary crates, workspaces and features. Where each boundary goes and what it buys you.
Pick the smallest boundary that solves the problem you actually have.
- Code is repeated → a function. Repeated with different types → a generic function.
- One file is too long, or unrelated things sit together →
modules, then one file per module.
pubonly on what the rest of the crate needs. - You want to test or reuse the logic → split the package into a
library crate and a thin
main.rs. - Several packages change together (a server, a CLI and a shared
core) → a workspace, with one
Cargo.lockand onetarget/. - Some users need a piece and others don't (a TLS backend, a serde integration) → a Cargo feature, off by default.
- Other people will use it → decide the public API on purpose: re-export what they need at the crate root and hide the rest.
Inside one crate
Section
Functions
The first boundary, and the one to reach for first.
Section
mod and files
A module inline, in a file, or in a folder, and how Rust finds each.
Section
Visibility
Private by default; pub, pub(crate) and pub(super) open it step by step.
Formal. Every item (function, type, constant, module, field, method)
is private by default: it can be used in the module that defines it and
in that module's descendants, and nowhere else. pub makes it visible
wherever its parent module is visible; pub(crate) anywhere in the same
crate; pub(super) in the parent module; pub(in path) inside the named
ancestor. A path is usable only if every module along it is visible from
the place it is written. Struct fields have their own visibility, so a
pub struct can keep every field private. Using an item that is not
visible is an error (E0603 for an item, E0616 for a field, E0624 for a
method).
In plain English. Everything starts locked inside the room it was
made in. Rooms inside that room can see in; nobody else can. pub
unlocks the door to whoever can already reach the room.
Example. Inside mod shop { pub struct Item { price: u32 } }, code
outside shop can name shop::Item but can't read item.price
(E0616) or build Item { price: 3 } (E0451).
Why this rule exists. Privacy lets a module keep promises: if only its own code can touch a field, only its own code can break the rules that field must follow, and the rest of the program can change freely around it.
An item is visible in its own module and the modules inside it, and nowhere else unless marked pub.
Section
Paths and use
crate::, self::, super::, and bringing names into scope.
Crates and packages
Section
Packages, crates and modules
The three words, and which one is the unit of compilation.
lib.rs holds the work, main.rs only calls it.
Section
Workspaces
Several packages, one lock file, one build directory.
Section
Features
Optional pieces a user switches on in Cargo.toml.
Formal. When several packages in one build depend on the same crate (at the same semver-compatible version), Cargo builds that crate once, with the union of every feature any of them requested. A feature therefore must only add functionality (new items, optional dependencies, extra impls); enabling it must never remove an item or change existing behavior in a way that breaks code written without it.
In plain English. If one part of your program switches a feature on, it is on for everyone who uses that crate. So a feature should only ever add things, like an optional extra, never take something away.
Example. If your app uses serde without derive and a dependency
enables serde/derive, your app's serde has derive too.
Why this rule exists. Building one copy of each crate keeps builds small and types compatible across the dependency graph; that only works if turning a feature on can't break anyone.
A feature must only add. Cargo unifies them across the whole build.
Shaping what others see
Section
Re-exports and API shape
pub use lets the public path differ from where the code lives.
Section
Designing a public API
Names, argument types, and what to leave out.
Section
Semver for libraries
Which changes to that API break callers.
Organizing by what a type has and needs, rather than by inheritance.