Organization
Rust organizes code through files, modules, crates, and workspaces. How you use these structures affects two things that matter as a project grows: development speed (how fast you can compile and iterate) and loose coupling (how easily you can change one part without breaking another).
Example of a Rust project’s organization, with a single workspace containing multiple crates.
Before we dive into this chapter, we should define what all of these terms mean.
| Name | Description |
|---|---|
| Module | Modules in Rust are used to hierarchically split code into logical units. Modules have a path, for example std::fs. Modules contain functions, structs, traits, impl blocks, and other modules. |
| File | A single source file, typically with a .rs extension. Every file is a module, but files can also contain inline (nested) modules. |
| Crate | Compilation unit in Rust. Can be a library crate or a binary crate, the latter require the presence of a main() function. They have an entrypoint, which is typically lib.rs or main.rs but can also be called something else. |
| Package | Collection of crates. Every package may contain at most one library crate, and may contain multiple binary crates. |
| Workspace | A collection of packages, which can share a build cache, dependencies and metadata. |
In this chapter, we will briefly cover how you can use these to structure your project.
Development Speed
Rust’s zero-cost abstractions produce fast binaries, but at the expense of compile times1. This tradeoff means that how you organize your project directly affects how fast you can iterate. A tight compile-test loop is essential for productive development, and the organizational choices in this chapter (splitting into crates, using workspaces, managing features) are the main levers you have to keep compile times under control as a project grows.
Loose Coupling
Large, monolithic codebases become difficult to change because everything depends on everything else. Splitting code into smaller, independent units with well-defined interfaces makes it easier to test components in isolation, assign ownership to different teams, and change implementations without cascading breakage. Rust’s module and crate system provides natural boundaries for achieving this2.
Reading
Chapter 7: Managing Growing Projects with Packages, Crates, and Modules by The Rust Programming Language
This chapter of The Rust Book shows you what facilities Rust has for structuring projects. It introduces the concepts of packages, crates and modules.
Chapter 2.5: Project Layout by The Cargo Book
This section in The Cargo Book explains the basic layout of a Rust project.
Rust at scale: packages, crates, modules (archived) by Roman Kashitsyn
Roman discusses how you can scale Rust projects, and what he has learned from participating in several large Rust projects. He gives some guidance on when to put things into modules versus into crates, and what implication this has on compile times. He also gives some advice on programming patterns, such as preferring run-time polymorphism over compile-time polymorphism. This article is a must-read for anyone dealing with a growing Rust project and it encodes a lot of wisdom that otherwise takes a long time to acquire.
Rust compile times by Matthias Endler
Matthias covers a wide range of strategies for reducing Rust compile times, from updating your toolchain and removing unused dependencies to splitting crates, using faster linkers, and optimizing CI with caching and cargo-nextest.
The Dark side of inlining and monomorphization by Nick Babcock
Nick explores how aggressive inlining and monomorphization can unexpectedly
bloat compiled artifacts. He demonstrates how a single #[inline(always)]
annotation on a large function caused massive code duplication across generic
instantiations, and shows how trait objects and removing inline hints reduced
binary size with negligible performance impact.
Delete Cargo Integration Tests by Alex Kladov
Alex argues for consolidating multiple integration test files into a single test crate. Each integration test file compiles into a separate binary that must be linked independently, and Cargo runs test binaries sequentially. When the Cargo project itself consolidated its integration tests, compile time dropped 3x and on-disk artifacts shrank 5x.