notes4 min read|2026-06-06

Rust Ownership Mental Model

A practical cheatsheet for choosing references, clones, Rc, Arc, Box, and interior mutability.

rustownershipcheatsheetnotes

The decision tree

When the compiler complains about ownership, walk through these questions in order:

Can the data flow be simpler?

Often the cleanest fix is rethinking the data flow. If two functions need to read the same data, pass references to both.

Borrow it

If the data outlives the place you need it, borrow it.

fn process(data: &[u32]) -> u32 {
    data.iter().sum()
}

let values = vec![1, 2, 3];
let total = process(&values);
// values still valid here

Use & for read access and &mut for write access. You can have many shared references or one mutable reference at a time.

Move it

If the caller doesn't need it anymore, move it.

fn consume(data: Vec<u32>) {
    // data is ours now
}

let values = vec![1, 2, 3];
consume(values);
// values is GONE, can't use it

Clone it

If both places need their own copy and the data is small, clone it.

let a = vec![1, 2, 3];
let b = a.clone();
// both a and b are valid, independent copies

Avoid cloning large data in hot paths. Cloning a Vec<u8> with 10MB of data copies 10MB. Cloning a 20-character String is usually fine. Let measurement guide the cleanup.

When to use smart pointers

TypeWhenOverhead
Box<T>Single owner, heap allocated1 pointer, no runtime cost
Rc<T>Multiple owners, single threadReference count (not atomic)
Arc<T>Multiple owners, multi threadAtomic reference count
Cow<'a, T>Maybe borrowed, maybe ownedEnum tag + data

Box

Use Box when you need a fixed-size type to hold a dynamically-sized thing, or when you have a recursive type.

enum Tree {
    Leaf(i32),
    Node(Box<Tree>, Box<Tree>),
}

Box gives the recursive branch a known pointer size, which lets the compiler compute the size of Tree.

Rc and Arc

Use Rc when multiple parts of your program need to read the same data and you can't figure out who should own it. Use Arc when those parts are on different threads.

use std::sync::Arc;

let config = Arc::new(load_config());
let config_clone = Arc::clone(&config);

std::thread::spawn(move || {
    // config_clone is valid here
    println!("{}", config_clone.database_url);
});

Arc::clone doesn't copy the data. It increments the reference count. The data is freed when the last Arc is dropped.

Cow

Use Cow when data is usually borrowed and occasionally needs an owned modified copy. It borrows when possible and clones on mutation.

use std::borrow::Cow;

fn normalize(input: &str) -> Cow<str> {
    if input.contains('\t') {
        Cow::Owned(input.replace('\t', "    "))
    } else {
        Cow::Borrowed(input)
    }
}

If the input has no tabs, zero allocation. If it does, one allocation. Perfect for functions that usually pass data through unchanged.

Interior mutability

When shared data needs controlled mutation:

TypeThread safePanics on misuseCost
Cell<T>NoNo (Copy types only)Zero
RefCell<T>NoYes, at runtimeBorrow flag
Mutex<T>YesNo (blocks)Lock overhead
RwLock<T>YesNo (blocks)Lock overhead
AtomicU64 etcYesNoAtomic ops

RefCell is the most common choice for single-threaded interior mutability. It moves Rust's borrow checking from compile time to runtime. Two overlapping mutable borrows cause a panic.

use std::cell::RefCell;

let data = RefCell::new(vec![1, 2, 3]);
data.borrow_mut().push(4);
println!("{:?}", data.borrow());

Prototyping with clones

When prototyping, clone freely. Get the logic right first, then remove unnecessary clones when you profile. The compiler is good at optimizing away unnecessary clones, and the ones that survive are usually cheap enough to leave alone.

Hot loops processing millions of items deserve more care because every allocation counts. Treat that as optimization work after the shape of the code is clear.

A character reading a Rust cookbook at a desk
Some notes are just there to keep the thread alive.