---
title: "Thinking in Rust: Ownership, Access, and Memory Safety"
description: "A mental framework for Rust's memory safety concepts. Think systematically about ownership, references, Send, Sync, and Rc, Arc, RefCell, Mutex, etc."
last_updated: 2025-10-10
doc_version: "2025-10-10"
canonical: https://cocoindex.io/blogs/rust-ownership-access/
---
# Thinking in Rust: Ownership, Access, and Memory Safety

> A mental framework for Rust's memory safety concepts. Think systematically about ownership, references, Send, Sync, and Rc, Arc, RefCell, Mutex, etc.

Published: 2025-10-10 · Canonical: https://cocoindex.io/blogs/rust-ownership-access/

I'm an experienced C++ programmer, but still feel that I need a mindset shift when starting to work with Rust.

- References can be immutable (`&`) or mutable (`&mut`), which is straightforward for the simplest cases. There are more complex cases, e.g. there are data types with interior mutability (e.g. `RefCell`), there are multi-level references (`& &mut`).
- Rust also models memory safety in multi-thread scenarios, so there's `Send` and `Sync`. They have intricate rules related to various types of references, e.g. when is `&T` `Send` or `Sync`? How about `&mut T`?
- There are various data types, like `Rc` , `Arc`, `Cell`, `RefCell`, `Mutex`, `RwLock`, `Cow` . When to pick which?

Each single rule is well documented from the bottom up, but when putting things together to design a complex system, it's easy to get overwhelmed by all these intricacies. So it would be very helpful if we had a clear mental model to help us think in a more rationalized way.

This article is not a tutorial to introduce these concepts, but provides a mental framework, as a simple and natural interpretation of Rust's memory safety models. I hope with this, Rust's rules around memory safety and capability of related [built-in data types](https://cocoindex.io/blogs/type-guided-serde) will be more easily understandable, based on rationalized coherent ideas instead of memorizing many rules.

## Basic concepts: ownership and access

A variable represents some resource in the memory. When talking about the relationship between the variable and the relevant resource, there are two types of roles: **ownership** and **access**.

```rust
let s: String = "Hello".to_string();  // Variable `s` owns the data
println!("{}", s);      // Variable `s` has access to the data

let p: &String = &s;    // Create a reference `p`
println!("{}", p);      // `p` also has access to the same data
```

### Access: shared vs exclusive

Rust classifies variables and their references as immutable (default) and mutable (`mut`). When it comes to some more intricate data types, the boundary is less clear. e.g. if my data type uses a cache to accelerate access, is it mutating things? What about some statistics collections? What about talking to a remote backend?

I find things become clearer suddenly after switching to a different viewpoint: ***thinking in terms of shared access and exclusive access***.

- **Shared Access** (default): You can access the resource, without excluding others from holding shared access simultaneously.
- **Exclusive Access** (`mut`): You have sole permission to use and modify the resource. No one else can access it during this period.

The level of access you have determines what you can do with the resource. Shared access is sufficient for read operations. Write operations require exclusive access. For methods of a resource, its receiver type (`&self` or `&mut self`) indicates what level of access is needed by this method. When you wrap a resource with something offering interior mutability (e.g. `Mutex` ), the wrapped resource can be accessed in a shared way, while its implementation provides run-time protection to only grant access exclusively at runtime.

*Exclusive access implies shared access*, i.e. what you can achieve with shared access, can always be achieved with exclusive access.

### Ownership implies lifetime keeper + exclusive access

When a variable owns a resource, there are two implications:

- **Lifetime Keeper**: You hold the lifetime of the resource. Before you're out of scope (e.g. end of the block that defines the variable), the ownership is either moved, or the resource needs to be dropped.
- **Access**: You have access to it through the entire lifetime of the ownership (unless during the period when it's lent exclusively).

Note that the access here can also be shared or exclusive, depending on whether you used `mut` when introducing it. However, even without `mut`, since you have the ownership, you can easily *upgrade* it to a mutable variable, e.g.

```rust
fn foo(s: String) {
  // You cannot do this, as you don't have exclusive access yet
  //! s.push_str(" world");

  // However, since you have ownership, it's easy to claim exclusive access
  let mut s = s;
  // Now we have exclusive access, so can modify it.
  s.push_str(" world");
  ...
}
```

So once you own a resource, whether or not you have `mut` isn't part of the contract. You have the same permission as exclusive access, i.e. ***ownership implies exclusive access***.

Lifetime keeping isn't another permission. For a resource, you can do almost everything with exclusive access: with `std::mem::take()` and `std::mem::replace()`, you can even drop the resource with exclusive access. Hence ***exclusive access is equivalent to ownership in terms of permission.*** Keep this in mind - it will be helpful to understand rules around `Send` and `Sync` later.

## Moves vs. borrows: transferring ownership or access

*Move* and *borrow* are two very unique concepts in Rust. Both are about passing around resources: one for ownership, one for access.

Rust provides two ways to pass around resources:

- **Move**: Permanently transfers ownership to another variable. It's a one-way ticket, and after moving, the original owner loses ownership and access.
    
    ```rust
    let s = "Hello".to_string();
    let t = s; // ownership moves to t; s is no longer valid
    ```
    
- **Borrow (Exclusive or Shared Access)**: Temporarily grants access to a reference without giving up ownership. It will suspend your exclusive access (as you no longer exclusively hold it) until the end of the borrow.
    
    ```rust
    let mut s = String::from("hello");
    let r = &mut s; // exclusive access borrowed
    r.push_str(" world");
    // Borrow ends here; s is usable again
    ```
    
    You can grant the access as long as you have the same level of access, even if you don't have the ownership. Once an exclusive access is lent out, your access is suspended until the borrow ends, by the definition of exclusivity. Once shared access is lent out, you only retain shared access, as you no longer exclusively hold the access.
    
:::note
Technically, when you grant access from `&T` or `&mut T`, slightly different things happen:

- It's called *reborrow* when you pass on access from `&mut T`.
- A *copy* happens when you grant access from `&T`, as shared access can be freely shared without suspending the current access.
:::

## Send and Sync: access across threads

Rust ensures thread-safety explicitly using two traits: `Send` and `Sync`. There are [3 rules](https://doc.rust-lang.org/std/marker/trait.Sync.html) around trait transitivity on `&T` and `&mut T`; however, anchoring them to the concepts of ownership and access will make things simpler to understand.

To define `Send` and `Sync`:

- **Send**: Indicates that ownership can safely be transferred to another thread. Hence when `T` is `Send`, `T` can be moved across the thread boundary.
    
    Corollary: As we discussed earlier, exclusive access is equivalent to ownership in terms of permission. So whenever we allow transferring ownership across threads, nothing can go wrong for shared access across the thread boundary, and vice versa. So `Send` also indicates the safety of transferring exclusive access across threads.
    
- **Sync**: Indicates shared access can be safely transferred across the thread boundary.

From here, we can reason about the 3 rules regarding trait transitivity:

1. By the definition of `Sync` above, `&T` is `Send` if and only if `T` is `Sync`.
2. By the definition and corollary of `Send` above, `&mut T` is `Send` if and only if `T` is `Send`.
3. For both `&T` and `&mut T`, their shared access (`& &T` and `& &mut T`) grants and only grants shared access of `T` . So `&T` and `&mut T` are `Sync` if and only if `T` is `Sync`.

You don't need to memorize any rules. Think clearly about which levels of access are allowed to happen across threads, and nothing can go wrong.

## Smart wrappers to defer compile-time safety to runtime

Rust tries hard to enforce safety at compile time, but it can be rigid sometimes. Some scenarios need more flexibility. Rust provides smart wrappers to defer compile-time safety guards to runtime:

- **Smart wrappers for interior mutability** (`Cell`, `RefCell`, `Mutex`, `RwMutex`). With these, only shared access on the wrapper is needed for exclusive access on the interior resource.
    - `Cell` is the simplest one, exposing no API to return references with exclusive access. The main API to set the value is:
        
        ```rust
        pub fn set(&self, val: T)
        ```
        
        It doesn't expose any exclusive access outside the function. Also, it declares to be `!Sync`, so all methods are guaranteed not to be called simultaneously. Hence it guarantees exclusive access isn't held concurrently without any extra runtime check.
        
    - `RefCell` also  declares to be `!Sync`, while it exposes methods that return proxy to access:
        
        ```rust
        // Expose shared access to T
        pub fn borrow(&self) -> Ref<T>
        // Expose exclusive access to T
        pub fn borrow_mut(&self) -> RefMut<T>
        ```
        
        While `Ref` and `RefMut` hold access to the interior resource  `T` after the methods return, it needs to perform a runtime check to make sure whenever `RefMut` is returned no other `Ref` or `RefMut` is live.
        
    - `Mutex` and `RwMutex` offer a similar interface to `RefCell`, but are both `Sync` - they're safely shared across threads. To achieve safety, it uses lock and read-write lock internally.
- **Smart wrappers for shared ownership** (`Rc` , `Arc` ). With these, ownership on the interior resource can be shared among multiple parties, and it's dropped when the last owner no longer holds the smart wrapper.  Shared ownership on the interior resource can only safely grant shared access. When it comes to exclusive access, they provide the following methods:
    
    ```rust
    pub fn get_mut(this: &mut Rc<T>) -> Option<&mut T>
    pub fn get_mut(this: &mut Arc<T>) -> Option<&mut T>
    ```
    
    which do a runtime check to make sure this is the sole owner, and return `None` otherwise. Internally, they do reference counting, while `Rc` is cheaper but thread unsafe (`!Send` + `!Sync`), and `Arc` is thread safe (`Send` + `Sync`) when `T` is thread safe (`Send` + `Sync`).
    
- **Smart wrappers for clone-on-write** (`Cow`) leave ownership versus shared access undecided at compile time. Shared access is always guaranteed without overhead. Whenever exclusive access is needed, it clones the interior resource.

When thinking from the angle of ownership and access, the purpose, contract, and behavior of these smart wrappers become more easily explainable. The choice comes down to two questions: are you sharing access or sharing ownership, and does it need to cross threads?

## Why this matters for a data engine

This mental model isn't academic for us — it's the daily reality of building CocoIndex's engine. The core is async-first on Tokio, which means values routinely cross thread boundaries: a future started on one worker thread can be polled to completion on another. The moment you put a value into a task that the runtime may move between threads, the compiler demands that value be `Send`. Reading that requirement through the ownership lens above — "`Send` means ownership (and therefore exclusive access) can safely move across a thread boundary" — turns a cryptic compiler error into an obvious design question: *should this resource really be handed off to another thread, and is everything it transitively owns safe to hand off too?*

The smart-wrapper choices fall out of the same reasoning. State that's genuinely shared across concurrent tasks — connection pools, cached schema, context values — lives behind `Arc`, because we need shared *ownership* with a lifetime that ends only when the last task lets go, and `Arc` is the thread-safe counter (`Rc` would be `!Send` and wouldn't compile into a multi-threaded task). Where that shared state also needs mutation, we reach for the `Sync` interior-mutability wrappers (`Mutex`/`RwLock`) so that shared access to the wrapper can yield exclusive access to the interior, with the runtime check enforcing exclusivity. The single-writer discipline around our internal [LMDB storage](https://cocoindex.io/docs/advanced_topics/internal_storage/) is exactly this principle applied at the I/O layer: many tasks hold shared access to the database handle, but writes are funneled so that mutation is serialized rather than racing.

The same separation of *what a value is* from *how it may be accessed* underpins our [type-guided serialization](https://cocoindex.io/docs/programming_guide/serialization/): a value the engine owns can be moved into the serializer and reconstructed on the other side, while a value merely borrowed can only be read. Thinking in ownership and access keeps these decisions principled instead of trial-and-error against the borrow checker.

## Conclusion

By clearly separating and defining **ownership** and **exclusive versus shared access**, Rust's complexity transforms into logical clarity. Moves, borrows, Send, Sync, and runtime checks become intuitive and predictable tools in your programming toolbox.

With this mental model, you can confidently explore even advanced Rust, while keeping your understanding grounded and practical.

## Support us
CocoIndex is an [open source](https://github.com/cocoindex-io/cocoindex) project built on Rust. Rust is the number one choice for any modern data engine, and these ownership and access ideas underpin how CocoIndex handles [type-guided serialization](https://cocoindex.io/docs/programming_guide/serialization/) and its [internal storage](https://cocoindex.io/docs/advanced_topics/internal_storage/). 
If this article is helpful to you, please drop a star ⭐ at [GitHub](https://github.com/cocoindex-io/cocoindex) to support this project.

## Sitemap

- [Blog index](https://cocoindex.io/blogs/)
- [Site index (llms.txt)](https://cocoindex.io/llms.txt)
- [Full blog corpus](https://cocoindex.io/llms-full.txt)
- [Markdown sitemap](https://cocoindex.io/sitemap.md)
- [XML sitemap](https://cocoindex.io/sitemap.xml)
- [RSS feed](https://cocoindex.io/blogs/rss.xml)
