notes4 min read|2026-06-06

Rust Ownership Mental Model

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

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

decision path

Ownership Route

1borrow

Use references when the original owner can keep the data alive.

2move

Transfer ownership when the caller is finished with the value.

3clone

Duplicate small or rare data when two places truly need it.

4share

Reach for Rc, Arc, or interior mutability only when structure demands it.

The decision tree

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

decision path

Ownership Route

1borrow

Use references when the original owner can keep the data alive.

2move

Transfer ownership when the caller is finished with the value.

3clone

Duplicate small or rare data when two places truly need it.

4share

Reach for Rc, Arc, or interior mutability only when structure demands it.

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.

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.

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.

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.

Cell
copy only
RefCell
runtime borrow
Mutex
blocking
Atomic
narrow
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.