Designing Memory‑Safe Systems with Rust’s Ownership and Borrowing Model
An architecture note on Rust's ownership and borrowing system: requirements, minimal design, trust boundaries, operational checks, failure modes, and when to reconsider the design. Includes a concrete buffer example and verification checklist.
01 Oct 2026, 14:22 UTC

Requirements
Systems that must run without a garbage collector need deterministic memory reclamation and zero data races. Rust satisfies these requirements by moving the responsibility for memory safety from runtime to compile time. The language guarantees:
- Each allocation has exactly one owner.
- References (borrows) are either shared (immutable) or exclusive (mutable), never both simultaneously.
- Every reference carries a lifetime that the compiler proves does not exceed the owner’s scope.
These rules eliminate double‑free, use‑after‑free, and data‑race bugs before the program executes.
Minimal Suitable Design
The smallest design that enforces the above constraints is a three‑part type system:
- Ownership types –
Box<T>,Vec<T>, or a plain struct field. When the owner goes out of scope,dropruns exactly once. - Reference types –
&T(shared) and&mut T(exclusive). The borrow checker enforces the Exclusive XOR Shared rule at each program point. - Lifetime parameters –
'aon functions and structs, allowing the compiler to relate the lifetime of a reference to the lifetime of its owner.
No runtime metadata (reference counts, GC headers) is required for the common case.
Trust and Data Boundaries
Ownership creates a clear trust boundary: the owner is the sole authority to mutate or deallocate the data. Borrowed references are untrusted – they may read (shared) or write (exclusive) but cannot outlive the owner. This boundary maps naturally to module boundaries: a public API can return owned values or references with explicit lifetimes, while internal implementation details remain hidden.
When data must be shared across threads, the type system forces the use of Arc<T> (atomic reference counting) combined with Mutex<T> or RwLock<T>. The borrow checker still governs access to the protected inner value, preserving the same compile‑time guarantees.
Operational Checks
Developers can verify ownership correctness without a full build:
# Run from the crate root (requires read permission on source files)
cargo check
cargo check runs the borrow checker and type inference, emitting errors such as “cannot borrow `buf` as mutable because it is also borrowed as immutable”. It does not produce binaries, so it is fast enough for continuous‑integration pipelines.
For deeper analysis, cargo miri (requires the miri component) interprets the MIR and can detect undefined behaviour that the borrow checker cannot see, such as misuse of unsafe blocks.
Concrete Example: A Simple Buffer
The following snippet demonstrates the exclusive‑or‑shared rule. It compiles on Rust 1.70+.
struct Buffer {
data: Vec<u8>,
}
impl Buffer {
fn read(&self) -> &[u8] {
&self.data
}
fn write(&mut self, src: &[u8]) {
self.data.extend_from_slice(src);
}
}
fn main() {
let mut buf = Buffer { data: vec![1, 2, 3] };
let slice = buf.read(); // immutable borrow
// buf.write(&[4, 5]); // ❌ compile error: mutable borrow while immutable borrow exists
println!("{:?}", slice);
buf.write(&[4, 5]); // ✅ allowed after `slice` goes out of scope
}
If the commented line is uncommented, cargo check reports:
error[E0502]: cannot borrow `buf` as mutable because it is also borrowed as immutable
--> src/main.rs:15:5
|
14 | let slice = buf.read();
| --- immutable borrow occurs here
15 | buf.write(&[4, 5]);
| ^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
The error pinpoints the exact conflict, letting the programmer restructure the code (e.g., split the read and write phases) without runtime cost.
Failure Modes
- Borrow‑checker fatigue – Early in a project, developers frequently encounter “cannot borrow” errors. The cost is development time, not runtime crashes.
- Graph‑like structures – Doubly‑linked lists, trees with parent pointers, or cyclic graphs cannot be expressed with pure ownership. The usual workaround is
Rc<RefCell<T>>(single‑thread) orArc<Mutex<T>>(multi‑thread), which moves some checks to runtime (reference‑count overflow, lock poisoning). - Excessive cloning – To appease the borrow checker, programmers may clone data unnecessarily, turning O(1) moves into O(n) copies and hurting performance.
- Unsafe escape hatches –
unsafeblocks bypass the borrow checker. If the invariants are violated (e.g., creating two mutable references to the same memory), the program exhibits undefined behaviour that the compiler cannot catch.
When to Re‑evaluate the Design
Consider changing the ownership strategy when:
- Profiling shows cloning or reference‑count overhead dominates a hot path.
- The data structure is inherently cyclic and the
Rc/RefCellpattern introduces runtime panics (e.g., double‑borrow at runtime). - Interoperability with C/C++ requires raw pointers; then a thin
unsafewrapper with documented invariants is appropriate. - Team experience indicates that the borrow‑checker learning curve is slowing delivery; a hybrid approach (safe core, unsafe FFI layer) may be pragmatic.
In each case, the decision should be documented with a risk assessment and a test plan (unit tests, cargo miri, fuzzing) to ensure the safety guarantees are not silently lost.
Practical Verification Checklist
- Run
cargo checkon every pull request – catches ownership errors early. - Enable
#![deny(unsafe_code)]inCargo.tomlto forbid accidentalunsafeunless explicitly allowed in a module. - Add
cargo miri testto CI for crates that containunsafeor useRc/RefCell. - Benchmark critical paths with
cargo benchbefore and after any cloning‑to‑borrow refactor. - Document lifetime contracts in public APIs (e.g.,
fn process<'a>(&'a self, input: &'a [u8]) -> &'a [u8]) so callers understand the borrow scope.
Limitations
The ownership model does not eliminate all memory‑safety concerns. It cannot prevent logic errors such as buffer over‑reads when using raw pointers inside unsafe, nor does it protect against memory exhaustion. Additionally, the compile‑time analysis is conservative; some safe programs are rejected, requiring refactoring or unsafe.
Version assumption: the examples target Rust 1.70 (2023‑06) and later, where non‑lexical lifetimes (NLL) are stable and the borrow checker follows the Exclusive XOR Shared rule precisely.