Modules and Crates in Rust: Organizing a Large Industrial Project
The Module System: mod and Visibility
Modules let you group related code and control what is visible to the rest of your program. By default, everything inside a module is private.
mod sensors {
// Private function -- only accessible inside this module
fn calibrate(raw: f64) -> f64 {
raw * 1.02 + 0.5
}
// Public function -- accessible from outside the module
pub fn read_temperature() -> f64 {
let raw = 71.3; // simulated raw ADC value
calibrate(raw) // can call private fn within the same module
}
pub fn read_pressure() -> f64 {
2.15
}
}
fn main() {
// We can call pub functions through the module path
let temp = sensors::read_temperature();
println!("Temperature: {:.1} C", temp);
// sensors::calibrate(50.0); // ERROR: calibrate is private
}
Use pub to expose functions, structs, and fields that other modules need. Keep internal details private to maintain clean boundaries.
File-Based Modules
As projects grow, you move modules into separate files. Rust supports two conventions:
Option A -- single file: Place code in src/sensors.rs and declare mod sensors; in src/main.rs.
Option B -- directory: Create src/sensors/mod.rs for the module root, then add sub-modules like src/sensors/temperature.rs.
src/
main.rs // mod sensors;
sensors/
mod.rs // pub mod temperature; pub mod pressure;
temperature.rs // pub fn read() -> f64 { ... }
pressure.rs // pub fn read() -> f64 { ... }
In src/sensors/mod.rs:
pub mod temperature;
pub mod pressure;
In src/sensors/temperature.rs:
pub fn read() -> f64 {
73.2 // simulated sensor reading
}
Both approaches are equivalent. The directory style works best when a module has several sub-modules.
use and Paths: Importing Items
The use keyword brings items into scope so you do not need to write the full path every time.
// Absolute path from crate root
use crate::sensors::temperature::read;
// You can rename imports to avoid collisions
use crate::sensors::temperature::read as read_temp;
use crate::sensors::pressure::read as read_press;
// Import multiple items from the same module
use std::collections::{HashMap, HashSet};
fn main() {
let temp = read_temp();
let press = read_press();
println!("Temp: {} C, Pressure: {} bar", temp, press);
}
Path types: crate:: starts from the current crate root, super:: refers to the parent module, and self:: refers to the current module.
Crates: Libraries and Binaries
A crate is the smallest compilation unit in Rust. There are two kinds:
- Binary crate -- has a
main.rswith afn main()entry point. Produces an executable. - Library crate -- has a
lib.rsthat exposes public items. Other crates depend on it.
A single package can contain both. For a factory monitoring system:
factory-monitor/
Cargo.toml
src/
main.rs // binary: runs the monitoring loop
lib.rs // library: shared types and logic
models.rs // pub struct Reading { ... }
In lib.rs:
pub mod models;
pub fn system_version() -> &'static str {
"1.0.0"
}
In main.rs:
use factory_monitor::models::Reading;
use factory_monitor::system_version;
fn main() {
println!("Factory Monitor v{}", system_version());
}
Cargo.toml: Dependencies and Features
Cargo.toml defines your crate metadata, dependencies, and optional features.
[package]
name = "factory-monitor"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
chrono = "0.4"
[features]
default = ["logging"]
logging = [] # custom feature flag
advanced-diagnostics = ["dep:some-diagnostics-crate"]
In your code, use #[cfg(feature = "logging")] to conditionally compile blocks:
#[cfg(feature = "logging")]
pub fn log_reading(sensor: &str, value: f64) {
println!("[LOG] {}: {}", sensor, value);
}
Run with features: cargo build --features advanced-diagnostics
Workspaces: Multi-Crate Projects
A workspace groups multiple related crates under one top-level Cargo.toml. All crates share a single target/ directory and lockfile. This is ideal for large industrial platforms.
# Root Cargo.toml
[workspace]
members = [
"crates/sensors",
"crates/alarms",
"crates/dashboard",
]
Directory structure:
factory-platform/
Cargo.toml # workspace root
crates/
sensors/
Cargo.toml # [dependencies] can reference siblings
src/lib.rs
alarms/
Cargo.toml
src/lib.rs
dashboard/
Cargo.toml # depends on sensors and alarms
src/main.rs
In crates/dashboard/Cargo.toml:
[dependencies]
sensors = { path = "../sensors" }
alarms = { path = "../alarms" }
Build everything at once with cargo build from the workspace root, or target one crate with cargo run -p dashboard.
Practical Example: Structuring a Factory Monitor
Here is a complete project layout for a factory monitoring system:
factory-monitor/
Cargo.toml # workspace
crates/
core/
Cargo.toml
src/lib.rs # shared types: Reading, MachineId, AlarmLevel
sensors/
Cargo.toml # depends on core
src/lib.rs
src/temperature.rs # pub fn poll(id: &str) -> Reading
src/pressure.rs
alarms/
Cargo.toml # depends on core
src/lib.rs
src/rules.rs # pub fn evaluate(r: &Reading) -> Option<Alarm>
app/
Cargo.toml # depends on sensors, alarms
src/main.rs # entry point, monitoring loop
In crates/core/src/lib.rs:
pub struct Reading {
pub machine_id: String,
pub value: f64,
pub unit: String,
}
pub enum AlarmLevel { Info, Warning, Critical }
In crates/app/src/main.rs:
use sensors::temperature;
use alarms::rules;
fn main() {
let reading = temperature::poll("FURNACE-01");
if let Some(alarm) = rules::evaluate(&reading) {
println!("ALARM on {}: level {:?}", reading.machine_id, alarm);
}
}
This structure keeps each concern isolated, testable, and reusable across different binaries.
Summary
- Modules (
mod) organize code into logical groups with controlled visibility viapub. - File-based modules scale projects by mapping modules to files and directories.
useimports items into scope, with support for renaming and grouping.- Crates are either binaries (
main.rs) or libraries (lib.rs), and a package can contain both. - Cargo.toml manages dependencies, versions, and feature flags for conditional compilation.
- Workspaces unify multi-crate projects under a single build, perfect for large industrial platforms.
- A well-structured project separates core types, sensor logic, alarm rules, and the application entry point into distinct crates.