Files
rust-patterns/patterns/module-organization.md
Rodin e1c2caf8d5 docs: module organization patterns from rust-lang/rust
10 patterns, 514 lines. Full spec compliance.
Patterns: one type per file, pub use re-exports, prelude module,
feature gates, API/impl separation, test organization, type aliases,
workspace multi-crate, conditional compilation, error modules.
2026-04-30 15:11:43 -07:00

515 lines
13 KiB
Markdown

# 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<T, io::Error> everywhere:
pub type Result<T> = std::result::Result<T, Error>;
// Instead of writing Box<dyn Fn(Request) -> Response + Send + Sync>:
pub type Handler = Box<dyn Fn(Request) -> 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
<!-- PATTERN_COMPLETE -->