August 21, 2026
I Stopped Fighting Rust’s Module System When I Understood What pub Actually Means
pub, pub(crate), and the moment I learned a struct can be public while lying about it

By Zeeshan Ali
5 min read
- 1 A module was never a file. The file is just where one happens to live.
- 2 Everything is private by default, and "everything" means more than you think
- 3 The doors between "everyone" and "only me"
- 4 The module tree and your public API don't have to be the same shape
- 5 Why this connects directly to keeping a project from turning into a mess
I made a struct public, pub struct Account, and Rust still told me one of its fields was private, from code sitting in the same crate, written by me, importing my own type. I remember staring at that error convinced the compiler had lost the plot. It hadn't. I had assumed pub was one setting, on or off, the way a light switch is. It's closer to a set of doors, and I'd only ever opened the first one.
I made a struct public, pub struct Account, and Rust still told me one of its fields was private, from code sitting in the same crate, written by me, importing my own type. I remember staring at that error convinced the compiler had lost the plot. It hadn't. I had assumed pub was one setting, on or off, the way a light switch is. It's closer to a set of doors, and I'd only ever opened the first one.
A module was never a file. The file is just where one happens to live.
The confusion starts early, usually with the old mod.rs convention, and it's worth clearing up before anything else makes sense: a Rust module is a node in a tree of names, and the file system is only one way of expressing that tree, not the thing itself. Writing mod garden; at the top of a file doesn't import a file. It declares a module named garden and tells the compiler to go looking for its contents, either in garden.rs sitting next to the file that declared it, or in garden/mod.rs, the older convention still found in plenty of tutorials and still perfectly valid today. Both name the exact same module. The file layout is a filing system for a tree that exists independently of it, and you can even skip files entirely:
mod garden {
pub struct Flower {
pub name: String,
}
}mod garden {
pub struct Flower {
pub name: String,
}
}That's a complete, valid module, nested inline, no separate file anywhere. The tree is the real thing. Files are just a convenient way of not writing your entire crate inside one enormous set of curly braces.
Everything is private by default, and "everything" means more than you think
Rust's default is strict: an item is only visible inside the module that defines it, and its child modules, unless you say otherwise. That much most people learn quickly. What catches people off guard is that this applies per-field, not just per-type, and a struct being pub says nothing at all about whether its fields followed it out the door.
pub struct Account {
pub id: u64,
balance: f64, // private, even though the struct itself is pub
}pub struct Account {
pub id: u64,
balance: f64, // private, even though the struct itself is pub
}Anyone outside this module can hold an Account and read its id. Nobody outside this module can touch balance directly, and that's true even for other code living in the same crate, as long as that code sits outside this module and outside any module nested inside it. Privacy in Rust is scoped to the module tree, not the crate as a whole: code inside accounts, or inside a child module declared within it, can still reach balance directly, because visibility flows downward through the tree from wherever an item is defined. A sibling module elsewhere in the same crate has no such access, and neither does anything outside the crate. This is precisely the trap I fell into: a public struct with a private field feels like a contradiction, but it's actually the language giving you exactly what encapsulation is supposed to give you, a public shape with private internals, and I'd been reading pub struct as a single, all-or-nothing decision instead of the first of several doors.
The doors between "everyone" and "only me"
pub alone means visible to literally anyone who depends on your crate, which is a much bigger promise than most internal code actually wants to make. Rust gives you narrower doors for narrower audiences:
pub(crate) fn internal_helper() { /* ... */ } // visible anywhere in this crate, nowhere outside it
pub(super) fn parent_only() { /* ... */ } // visible only to the parent module
pub(in crate::garden) fn scoped() { /* ... */ } // visible only within a specific module path you namepub(crate) fn internal_helper() { /* ... */ } // visible anywhere in this crate, nowhere outside it
pub(super) fn parent_only() { /* ... */ } // visible only to the parent module
pub(in crate::garden) fn scoped() { /* ... */ } // visible only within a specific module path you namepub(crate) is the one that does the most quiet, everyday work once you know it exists. It's the honest way to say "this is an implementation detail other parts of my own crate are allowed to reach into, but nobody downloading this crate ever should," which is a genuinely common shape and one plain pub overstates every time you reach for it out of habit rather than intent.
The module tree and your public API don't have to be the same shape
This is the part that took the module system from "a set of rules I follow" to "a tool I actually design with." Nothing requires your internal file structure to match the API you want people to see. use, combined with pub, lets you re-export an item somewhere completely different from where it actually lives:
// deep inside src/garden/soil/nutrients.rs
pub struct NutrientProfile { /* ... */ }
// in src/lib.rs, the crate root
pub use garden::soil::nutrients::NutrientProfile;// deep inside src/garden/soil/nutrients.rs
pub struct NutrientProfile { /* ... */ }
// in src/lib.rs, the crate root
pub use garden::soil::nutrients::NutrientProfile;Anyone depending on this crate can now write use my_crate::NutrientProfile; directly, with no idea, and no need to know, that it actually lives three folders deep next to code about soil composition. You get to organize your files around what makes sense to you while presenting a clean, flat, deliberately designed surface to everyone else. The file tree is for your convenience. The re-exports are the actual conversation you're having with anyone who uses your crate.
Why this connects directly to keeping a project from turning into a mess
How to Structure a Rust Project Before It Becomes a Mess Modules, crates, visibility rules, and the conventions that keep a growing Rust codebase readable.
If a project's file layout was ever going to spiral into confusion, this is exactly where it happens: reaching for pub everywhere out of impatience, because a narrower visibility keyword produced an error and pub made the error go away. Every one of those decisions is a real commitment once other people depend on your crate. For a library, exposing something publicly is an API commitment: removing or restricting that item later can become a breaking change for downstream users. pub(crate) costs nothing extra to write and closes that door before it ever needs closing.
The module tree, the field-level privacy, and re-exports aren't three separate features to memorize. They're one coherent answer to a single design question: what should the outside world be allowed to touch, and what's mine to change without asking anyone's permission first. Once that clicked, the compiler complaining that my public struct had a private field stopped looking like a contradiction. It was the one part of the whole system that had been trying to teach me the actual lesson from the very first error message.
If you're prepping for interviews or just want the fast reference: Rust vs. Java — The Data Structures Cheat Sheet is free — Big-O for every structure, the full 55-problem interview index by chapter, and the three rules that separate a senior answer from a junior one.
If you're curious how these same visibility and API-design questions play out on the Java side, that's the whole premise of the book I've been writing: every classic data structure built once in Rust and once in Java, side by side, so you can see exactly where each language's rules force a different decision. Ownership vs. Reference: Data Structures and Algorithms in Rust and Java — $9 · Free preview
For the Rust foundations that series builds on: Zero to Rust: A Systems Programmer's Field Guide — $7 · Free 20-page sample