Ownership Best Practices
Rules for APIs and data structures that work with the borrow checker instead of against it.
How to Use This List
- Apply during API design reviews before publishing a crate.
- Use when borrow errors repeat in the same module.
- Pair with clippy lints like
redundant_cloneandneedless_borrow.
A - API Shape
- Take
&str,&[T],&Pathfor read-only inputs. Callers keep ownership; fewer moves. - Return owned values unless zero-copy is a measured win. Simpler lifetimes for consumers.
- Use
impl AsRef<str>at boundaries sparingly. Convenience for callers; document coercion. - Prefer
Cow<'a, T>for maybe-borrowed/maybe-owned text. Avoids forced allocation. - Document who owns returned handles. Callers need to know drop responsibility.
B - Mutation and Sharing
- Use
&mut selfmethods for in-place updates. Clear exclusive access contract. - Reach for interior mutability only after structural refactors fail.
RefCell/Mutexlast resort. - Split collections and indices for graphs.
usizeintoVecbeats self-references. - Avoid
Rcuntil shared ownership is required. Single owner is simpler to reason about. - Do not hold locks across
.await. Use async-aware primitives (tokio::sync).
C - Cloning and Copying
- Clone at boundaries, not in inner loops. Profile before optimizing clones away.
- Derive
Copyonly for small, stack-only types. LargeCopyarrays are expensive. - Use
clone_fromwhen reusing allocation.dst.clone_from(&src)may reuse capacity. - Enable
clippy::redundant_clone. Catches needless duplicates in review. - Treat
to_owned()likeclone()for clarity. Pick one style per crate.
D - Lifetimes in Public APIs
- Minimize lifetime parameters on public structs. Each
'ais cognitive cost for users. - Prefer elision where compiler allows. Add explicit lifetimes when errors guide you.
- Never leak stack borrows into threads without scoped threads. Use
'staticowned captures. - Explain lifetime constraints in rustdoc. Future maintainers need the why.
- Add examples that compile in doctests. Lifetime APIs break silently otherwise.
FAQs
How many clones are too many?
If profiling shows clone dominates, refactor. Otherwise clarity wins at boundaries.
When is `Arc` appropriate?
Shared ownership across threads or long-lived caches with multiple handles.
Should library APIs use `String` or `&str`?
Inputs: &str. Outputs: owned String unless borrowing from input is intentional and documented.
How to review ownership in PRs?
Check function signatures, new Rc/RefCell, clones in loops, and unwrap on locks.
Acceptable `unwrap` on `RefCell`?
Only when invariants prove no re-entrant borrow - document invariant.
Workspace-wide conventions?
Publish an ADR on string types, error ownership, and async state patterns.
Testing borrow-heavy code?
Doctests, Miri for unsafe adjacency, loom for concurrency (advanced).
Newtype for IDs?
Prevents mixing indices from different registries - cheap win.
When to pin?
Self-referential async/state machines - follow pin crate patterns, do not invent.
Next topics?
Structs/enums for modeling, smart pointers when sharing is real.
Related
- Ownership Basics - fundamentals
- Fighting the Borrow Checker - refactor patterns
- Smart Pointers Best Practices -
Rc/Arcrules - Structs & Enums Best Practices - modeling data
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.