As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!
Let’s talk about keeping your code tidy. When I first started programming, I’d write everything in one long, sprawling file. It worked, for a while. Then the project grew. Finding anything became a treasure hunt, and changing one thing would mysteriously break three others. It was a mess. Rust offers a way out of that mess: its module system. Think of it as a set of rules and tools for putting your code in the right boxes, closing the lids, and labeling them clearly. It’s how you tell the compiler—and other developers—what goes together and what should stay separate.
At its heart, a module is just a namespace. It’s a named container for your functions, structs, enums, and other items. By putting code inside a module, you’re saying, “This stuff belongs together.” This does two wonderful things. First, it organizes your thoughts. Second, it prevents name collisions. You can have a connect function in a network module and a completely different connect function in a database module, and there’s no confusion.
Here’s the simplest way to make one. You use the mod keyword.
mod communications {
pub fn send_message(msg: &str) {
println!("Sending: {}", msg);
}
}
fn main() {
communications::send_message("Hello, modules!");
}
That pub keyword is crucial. By default, everything inside a module is private, hidden from the outside world. If I removed pub from that function, the main function wouldn’t be able to call it. The compiler would stop me. This is Rust helping me enforce boundaries. I must explicitly decide what to make public. This default privacy is a gentle push toward better design. It makes me think about my public interface—the parts other code should rely on—and hide the messy implementation details that might change later.
Let’s build a slightly more realistic example. Imagine a part of an application that handles configuration.
mod config {
// This struct is public, but its fields are private.
pub struct Settings {
timeout: u32,
retries: u8,
}
impl Settings {
// A public constructor
pub fn new() -> Self {
Settings {
timeout: 30,
retries: 3,
}
}
// Public getter methods
pub fn timeout(&self) -> u32 {
self.timeout
}
// A public setter that validates input
pub fn set_timeout(&mut self, seconds: u32) -> Result<(), String> {
if seconds > 300 {
return Err("Timeout cannot exceed 300 seconds".to_string());
}
self.timeout = seconds;
Ok(())
}
}
}
fn main() {
let mut my_config = config::Settings::new();
println!("Default timeout: {}", my_config.timeout());
match my_config.set_timeout(60) {
Ok(()) => println!("Timeout updated."),
Err(e) => println!("Error: {}", e),
}
// This won't compile! The fields are private.
// my_config.timeout = 100;
}
See what happened there? The Settings struct is public, so I can create one. But its fields are private. I can’t modify timeout directly. I have to use the public set_timeout method, which contains validation logic. The module allows me to expose a safe, controlled way to interact with my configuration, while keeping the data itself and any complex rules locked away inside. The caller gets simplicity and safety.
Now, writing everything in one file inside mod blocks gets old fast. The real power comes from linking modules to your file system. Each file is a module. Let’s split the example above. I’ll create a file named config.rs.
// File: config.rs
pub struct Settings {
timeout: u32,
}
impl Settings {
pub fn new() -> Self {
Settings { timeout: 30 }
}
pub fn timeout(&self) -> u32 {
self.timeout
}
}
Now, in my main.rs, I declare that I want to use this module. It’s like telling the compiler, “Go look for a module named config.”
// File: main.rs
mod config; // This declares the module, loading it from config.rs
fn main() {
let settings = config::Settings::new();
println!("Timeout is: {}", settings.timeout());
}
This is how you start to structure a project. It feels natural. A file called network.rs probably holds networking code. A file called database.rs holds database logic. The module hierarchy in your code mirrors the folder hierarchy on your disk.
What about when you have a lot of related code? You use a directory. Let’s say my networking code grows. I want a network module with sub-modules for tcp and udp. I would create a structure like this:
src/
├── main.rs
└── network/
├── mod.rs
├── tcp.rs
└── udp.rs
The network directory’s presence signals it’s a module. The mod.rs file is the entry point for that module. Here’s what these files might contain:
// File: src/network/mod.rs
pub mod tcp; // Declare the tcp sub-module
pub mod udp; // Declare the udp sub-module
pub fn protocol_version() -> &'static str {
"1.1"
}
// File: src/network/tcp.rs
pub struct Connection {
socket_id: i32,
}
impl Connection {
pub fn open(address: &str) -> Result<Self, String> {
// ... complex logic ...
Ok(Connection { socket_id: 42 })
}
}
// File: src/network/udp.rs
pub fn send_datagram(data: &[u8]) {
// ... UDP-specific logic ...
}
Finally, in main.rs, I can reach into this hierarchy.
// File: src/main.rs
mod network;
fn main() {
println!("Using protocol: {}", network::protocol_version());
let tcp_conn = network::tcp::Connection::open("127.0.0.1:8080").unwrap();
network::udp::send_datagram(b"hello");
}
The path network::tcp::Connection clearly shows the organization. It’s self-documenting. If a new developer joins the project and needs to work on TCP logic, they know exactly where to look.
Constantly writing out these full paths can be verbose. That’s where the use keyword comes in. It brings a path into scope, so you can refer to it by a shorter name.
mod network;
// Bring the TCP Connection type into scope
use network::tcp::Connection;
fn main() {
// Now I can just use `Connection`
let conn = Connection::open("127.0.0.1:80");
}
You can be selective. use network::tcp; would let me write tcp::Connection. use network::tcp::Connection; lets me write just Connection. I prefer being explicit with my use statements. It makes it clear where each item in my file comes from, which is a huge help when reading code later. Wildcard imports like use network::*; are possible but I avoid them. They can silently cause name conflicts and make the origin of things ambiguous.
The module system fundamentally changes how you write libraries. As a library author, my goal is to present a clean, stable, and easy-to-understand public API. Everything else is an implementation detail. Modules are my tool for doing that. I put the public types and functions I want users to call in the root of my library or in clearly marked public modules. All the helper functions, internal data structures, and complex algorithms go into private modules or are marked as private within public modules.
Here’s a tiny snippet from what a graphics library’s structure might look like internally:
// File: src/lib.rs (the library root)
pub mod shapes {
pub mod circle;
pub mod rectangle;
}
mod renderer { // A private internal module
fn triangulate_polygon(vertices: &[Point]) -> Vec<Triangle> {
// Complex, private geometry processing
// ...
}
}
pub fn draw_scene() {
// This public function can use the private `renderer` module
// let mesh = renderer::triangulate_polygon(...);
}
A user of my library can write use my_graphics_lib::shapes::circle;. They have no idea the renderer module even exists, and I am free to change or rewrite it completely without breaking their code. The module boundary acts as a firm contract.
This brings me to a key point: testing. How do you test private functions? Rust has a clever answer. Tests are just code that needs to access your modules. You can write a test module inside the module you want to test. The #[cfg(test)] attribute tells the compiler to only include this code when running cargo test.
mod my_internal_math {
pub fn public_api(x: i32) -> i32 {
private_helper(x) * 2
}
// This function is private, for internal use only.
fn private_helper(x: i32) -> i32 {
x + 5
}
#[cfg(test)] // This module only exists during `cargo test`
mod tests {
use super::*; // Bring everything from the parent module into scope
#[test]
fn test_private_helper() {
// Because tests are inside the parent module,
// they can access private items.
assert_eq!(private_helper(10), 15);
}
#[test]
fn test_public_api() {
assert_eq!(public_api(10), 30); // (10 + 5) * 2
}
}
}
This is elegant. It keeps tests close to the code they test, and it allows thorough testing of internal logic without having to expose that logic publicly. When I build the library for release, the test module is stripped out.
For very large projects, a single crate (a binary or library) might still feel too big. This is where Cargo workspaces come in. A workspace is a set of multiple crates that share a Cargo.lock file and an output directory. You can have a workspace with a binary crate (my_app) and several library crates (my_core, my_network, my_ui). Each of these crates has its own src directory and its own module tree. They are developed and versioned together but maintain very strong boundaries—they can only access each other’s public APIs, just like external libraries.
This is the module system scaling up. It enforces clear, compile-time-checked contracts between different parts of a large system. When I work on the my_network crate, I don’t need to know anything about the my_ui crate’s internals. I just need to know its public API. This is how large teams can work on a single Rust project without constantly stepping on each other’s toes.
In the end, that’s what Rust’s module system gives you: control and clarity. It’s a way to manage complexity by imposing a helpful structure. It starts with a simple mod keyword in a single file and grows with your project, all the way up to multi-crate workspaces. The compiler is your partner in this, enforcing the privacy rules you set up. It stops you from accidentally creating hidden, tangled dependencies. You have to be intentional about how pieces connect. This intentionality, forced upon you by the language, is what leads to code that remains readable, maintainable, and adaptable long after it’s first written. It turns the potential for a sprawling mess into a well-organized library, where everything has its place.
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | Java Elite Dev | Golang Elite Dev | Python Elite Dev | JS Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva
Top comments (0)