# Rust Module Organization Patterns Patterns for organizing code into modules, extracted from the standard library source. **Source:** [rust-lang/rust](https://github.com/rust-lang/rust) at commit [`f53b654`](https://github.com/rust-lang/rust/tree/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6) **Stats:** 220 mod.rs files, 41 lib.rs files, 1,404 module declarations, 919 pub(crate) items, 18,430 pub fn. --- ## 1. One Type Per File (Large Types) ### Source: [library/alloc/src/string.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/alloc/src/string.rs) (String), [library/alloc/src/vec/mod.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/alloc/src/vec/mod.rs) (Vec) ```text alloc/src/ ├── string.rs ← String type + all impls (3000+ lines) ├── vec/ │ ├── mod.rs ← Vec type + core methods │ ├── drain.rs ← Drain iterator │ ├── into_iter.rs ← IntoIter │ ├── splice.rs ← Splice iterator │ └── ... └── collections/ ├── btree/ │ ├── map.rs │ ├── set.rs │ └── ... └── ... ``` ### Why Large types get their own file (or directory). This keeps files navigable. When a type has many associated iterators or helpers, it becomes a directory with `mod.rs`. The split is by related type, not by "kind" (don't put all iterators in `iterators.rs`). ### When to Use **Triggers:** - A type + impls exceeds ~500 lines - A type has multiple associated types (iterators, builders) - Multiple people work on the same module (reduce conflicts) **Example — before:** ```rust // src/collections.rs — 5000 lines, everything jammed together pub struct HashMap { ... } impl HashMap { /* 800 lines */ } pub struct HashSet { ... } impl HashSet { /* 600 lines */ } pub struct BTreeMap { ... } // Impossible to navigate ``` **Example — after:** ```rust // src/collections/mod.rs pub mod hash_map; pub mod hash_set; pub mod btree_map; // src/collections/hash_map.rs pub struct HashMap { ... } impl HashMap { ... } ``` ### When NOT to Use **Don't use this when:** - The type is small (<100 lines with impls) - Related types are tightly coupled and small (keep together) --- ## 2. Re-exports for Clean Public API (pub use) ### Source: [library/std/src/lib.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/std/src/lib.rs) ```rust // Internal organization: mod io; mod fs; mod net; // Public API — re-export the important bits: pub use io::{Read, Write, BufRead, Seek}; pub use fs::{File, OpenOptions}; ``` ### Why Internal module structure serves implementation needs. Public API structure serves user needs. They're often different. `pub use` lets you organize internally however makes sense while presenting a clean, flat API to users. ### When to Use **Triggers:** - Internal modules are deeply nested but users want flat access - You want `use my_crate::ImportantType` instead of `use my_crate::internal::deep::nested::ImportantType` - Reorganizing internals without breaking the public API **Example — before:** ```rust // Users must know internal structure: use my_crate::network::http::client::HttpClient; use my_crate::network::http::request::Request; use my_crate::network::http::response::Response; ``` **Example — after:** ```rust // lib.rs re-exports at the crate root: pub use network::http::client::HttpClient; pub use network::http::request::Request; pub use network::http::response::Response; // Users just write: use my_crate::{HttpClient, Request, Response}; ``` ### When NOT to Use **Don't use this when:** - The module hierarchy IS the API (users should know about it) - Re-exports would cause name collisions - You're re-exporting too much (defeats the purpose of modules) --- ## 3. Prelude Module for Common Imports ### Source: [library/std/src/prelude/mod.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/std/src/prelude/mod.rs) ```rust pub mod prelude { pub mod v1 { pub use crate::option::Option::{self, Some, None}; pub use crate::result::Result::{self, Ok, Err}; pub use crate::iter::Iterator; pub use crate::clone::Clone; // ... most commonly used traits and types } } ``` ### Why A prelude gathers the most commonly used items so users can do `use my_crate::prelude::*` instead of 20 individual imports. The stdlib prelude is auto-imported; library preludes are opt-in. ### When to Use **Triggers:** - Your library has 5+ types/traits that users import together - Most users need the same set of imports - Reducing boilerplate matters for ergonomics **Example — before:** ```rust // Every file in the project: use my_framework::{App, Context, Request, Response, Handler}; use my_framework::middleware::{Logger, Cors, Auth}; use my_framework::error::Error; // 3 lines of imports every single time ``` **Example — after:** ```rust // my_framework/src/prelude.rs pub use crate::{App, Context, Request, Response, Handler}; pub use crate::middleware::{Logger, Cors, Auth}; pub use crate::error::Error; // Every file in the project: use my_framework::prelude::*; ``` ### When NOT to Use **Don't use this when:** - The library is small (few imports anyway) - Items would conflict with common names (String, Error) - You want explicit imports for clarity --- ## 4. Feature Gates (#[cfg(feature = "...")]) ### Source: The stdlib uses extensive feature gating for unstable features. Libraries use it for optional dependencies. ```rust #[cfg(feature = "serde")] impl Serialize for MyType { ... } #[cfg(feature = "async")] pub mod async_client { ... } ``` ### Why Feature gates let you provide optional functionality without imposing dependencies on all users. Users opt in via Cargo.toml. This keeps the default compilation fast and the dependency tree small. ### When to Use **Triggers:** - Optional dependencies (serde, tokio, etc.) - Optional functionality that not all users need - Platform-specific code - Unstable/experimental APIs **Example — before:** ```rust // Everyone pays the compile cost of serde even if they don't use it use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize)] pub struct Config { ... } ``` **Example — after:** ```rust // Cargo.toml: [features] serde = ["dep:serde"] #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Config { ... } // Only compiled with serde when the feature is enabled ``` ### When NOT to Use **Don't use this when:** - The functionality is core (everyone needs it) - Feature interaction is complex (too many combinations to test) - The dependency is tiny (not worth the complexity) --- ## 5. Separating Public API from Implementation ### Source: ```text std/src/io/ ├── mod.rs ← public API, re-exports, module docs ├── buffered/ ← implementation details │ ├── bufreader.rs │ ├── bufwriter.rs │ └── linewriter.rs ├── cursor.rs ← Cursor implementation ├── error.rs ← Error types └── impls.rs ← trait implementations ``` ### Why The `mod.rs` (or parent module) defines WHAT users see. Sub-modules contain HOW things work. This separation means you can refactor internals without changing the public API. ### When to Use **Triggers:** - Module has both public API and complex implementation - You want to reorganize implementation without breaking users - The implementation has multiple sub-components ### When NOT to Use **Don't use this when:** - The module is simple (one file is fine) - Implementation IS the API (no separation needed) --- ## 6. tests/ Subdirectory for Test Organization ### Source: ```text library/ ├── core/ │ └── src/ │ └── option.rs ← implementation ├── coretests/ │ └── tests/ │ └── option.rs ← tests for core::option └── alloctests/ └── tests/ └── vec.rs ← tests for alloc::vec ``` ### Why The stdlib separates tests into their own crates (`coretests`, `alloctests`) because `core` can't depend on `alloc` for testing. For normal projects, `tests/` directory provides the same separation for integration tests. ### When to Use **Triggers:** - Integration tests (testing the public API) - Tests that need external resources - Tests that should be separate binaries (parallelism) --- ## 7. Type Aliases for Complex Generics ### Source: ```rust // Instead of writing Result everywhere: pub type Result = std::result::Result; // Instead of writing Box Response + Send + Sync>: pub type Handler = Box Response + Send + Sync>; ``` ### Why Type aliases reduce visual noise for complex generic types. They make function signatures readable and provide a single point of change if the underlying type evolves. ### When to Use **Triggers:** - A generic type appears in 3+ signatures - The full type is > 40 characters - You want a domain-specific name for a generic composition ### When NOT to Use **Don't use this when:** - The alias hides important information (what IS that thing?) - The alias is used in only one place - The original type is already short and clear --- ## 8. Workspace Organization (Multi-Crate Projects) ### Source: The Rust stdlib itself is organized as multiple crates: ```text library/ ├── core/ ← no allocator, no OS (can run on bare metal) ├── alloc/ ← needs an allocator (Vec, String, Box) ├── std/ ← needs an OS (files, threads, networking) └── proc_macro/ ← procedural macro support ``` ### Why Multiple crates within one workspace provide: - Separate compilation (parallel builds) - Clear dependency boundaries (core doesn't depend on alloc) - Different feature sets per crate - Independent versioning ### When to Use **Triggers:** - Different parts of your project have different dependencies - You want parallel compilation (workspace members build in parallel) - Some code is reusable independently (separate crate = separate publish) - Clear architectural boundaries ### When NOT to Use **Don't use this when:** - The project is small (<2000 lines) - Everything depends on everything else (no natural boundaries) - The complexity of multiple crates outweighs the benefits --- ## 9. Conditional Compilation (#[cfg]) ### Source: Extensive use of `#[cfg(target_os)]`, `#[cfg(feature)]`, `#[cfg(test)]` throughout the stdlib. ```rust #[cfg(unix)] mod unix_impl; #[cfg(windows)] mod windows_impl; #[cfg(target_pointer_width = "64")] type Repr = repr_bitpacked::Repr; #[cfg(not(target_pointer_width = "64"))] type Repr = repr_unpacked::Repr; ``` ### Why `#[cfg]` compiles code conditionally. Dead code is eliminated at compile time — no runtime cost. This is how one codebase supports multiple platforms, feature sets, and configurations. ### When to Use **Triggers:** - Platform-specific implementations (OS, architecture) - Feature-gated code - Test-only code (`#[cfg(test)]`) - Debug-only code (`#[cfg(debug_assertions)]`) ### When NOT to Use **Don't use this when:** - Runtime feature detection is needed (cfg is compile-time only) - The conditional logic is simple enough for `if cfg!(...)` macro - You're using cfg to hide broken code (fix it instead) --- ## 10. Error Modules ### Source: ```text std/src/io/ ├── error.rs ← Error type definition ├── error/ │ ├── repr_bitpacked.rs ← compact representation │ └── repr_unpacked.rs ← standard representation └── mod.rs ← re-exports Error ``` ### Why Error types often need their own module when they have: - Multiple representations - Many From implementations - Complex display logic - Associated helper types ### When to Use **Triggers:** - Error type exceeds 100 lines - Error has multiple variants with different data - Error needs platform-specific representations --- ## Summary: Module Organization Decision Tree ``` How big is the module? ├── <200 lines → single file ├── 200-1000 lines → file with sections └── >1000 lines → directory (mod.rs + sub-modules) What should users see? ├── Everything → pub mod (expose structure) ├── Flat API → pub use (re-export from depth) └── Common subset → prelude module Platform-specific code? ├── cfg(unix) / cfg(windows) → parallel mod files └── cfg(feature) → optional modules Test organization? ├── Unit tests → #[cfg(test)] mod tests (same file) ├── Integration → tests/ directory └── Shared helpers → tests/common/mod.rs ``` | Pattern | Use when | |---|---| | One type per file | Large types (>500 lines with impls) | | `pub use` re-exports | Clean API surface hiding internal structure | | Prelude module | 5+ commonly imported items | | Feature gates | Optional functionality/dependencies | | API/impl separation | Public surface ≠ implementation structure | | `tests/` directory | Integration testing | | Type aliases | Complex generics appearing multiple times | | Workspace (multi-crate) | Clear architectural boundaries | | `#[cfg]` | Platform/feature conditional compilation | | Error modules | Complex error types | See also: - [api-design.md](api-design.md) — Public API design patterns - [documentation.md](documentation.md) — Module-level documentation