If you've worked in languages like C++ or Python, Rust's ownership system might feel alien at first—but it's the foundation of why Rust code is both memory-safe and blazingly fast. We'll walk through the core ownership rules with concrete examples so you can write Rust confidently in production.
The core ownership model
Rust ownership explained: a hands-on guide for engineers starts with understanding that every value in Rust has exactly one owner at any given time. This isn't a garbage collector deciding when to free memory; it's a compile-time contract. The owner is responsible for cleaning up the value. When ownership changes hands, the previous owner can no longer access that value.
This single-owner constraint removes entire categories of bugs. No use-after-free. No double-free. No data races. The compiler verifies all of this before your code ships.
Think of it like a car title. Only one person owns the car. When you sell it, the title transfers. You can't drive a car you no longer own—the compiler won't let you.
One owner—Each value has exactly one owner
In Rust, when you bind a value to a variable, that variable becomes the owner. Let's look at a real scenario from a web backend where you're processing user data:
fn main() {
let user_name = String::from("alice@example.com");
// user_name is the owner of the String
println!("User: {}", user_name); // OK
// user_name still owns the value here
}
// user_name goes out of scope, the String is freed automatically
The variable user_name owns the heap-allocated String. As long as it's in scope, you can use it. When the function returns or the block ends, that ownership is released and memory is reclaimed.
Now let's add a function call to this pattern. When you pass a value to a function, you're also passing ownership:
fn log_user(name: String) {
println!("Logging: {}", name);
// name is owner here
}
// name goes out of scope, String is freed
fn main() {
let user = String::from("bob@example.com");
log_user(user);
// After this call, user no longer owns the String
// println!("{}", user); // ERROR: value used after move
}
Once you pass user to log_user, you've transferred ownership. The function parameter becomes the new owner. When the function ends, that memory is freed. You can't use user afterward—the compiler catches this at compile time.
Moves—Assigning a String moves ownership
When you assign one variable to another, ownership moves rather than copies. This is the behavior that catches many developers off guard coming from languages without explicit ownership:
fn main() {
let a = String::from("hi");
let b = a; // a is moved to b
// println!("{a}"); // error: use of moved value
println!("{b}"); // OK: b is now the owner
}
After let b = a;, the ownership of that String has moved from a to b. Variable a is no longer valid. If you tried to use it, the compiler would reject your code with a clear error message: "value used after move."
This is powerful in production systems because it forces you to think about resource lifetime. Let's see a more realistic example from an e-commerce order pipeline:
struct Order {
id: u64,
items: Vec<String>,
}
fn process_order(order: Order) {
// order is now owned by this function
println!("Processing order {}", order.id);
// Order is freed here when the function ends
}
fn main() {
let my_order = Order {
id: 12345,
items: vec!["widget".to_string(), "gadget".to_string()],
};
process_order(my_order); // ownership moves
// my_order is no longer accessible
// println!("{:?}", my_order); // ERROR
}
Once my_order is passed to process_order, the function takes ownership. When process_order completes, the Order and all its heap-allocated data are cleaned up automatically. No memory leaks, no dangling pointers.
Drop—Values are freed when the owner goes out of scope
Rust calls the cleanup method Drop. For most standard types, the compiler automatically implements this. When a value goes out of scope, drop is called, and memory is reclaimed.
fn main() {
{
let buffer = String::from("temporary data");
println!("{}", buffer); // OK
} // buffer goes out of scope, drop is called, memory freed
// println!("{}", buffer); // ERROR: not in scope
}
You can manually implement Drop for custom types to clean up resources like database connections or file handles:
struct DbConnection {
url: String,
}
impl Drop for DbConnection {
fn drop(&mut self) {
println!("Closing connection to {}", self.url);
// Actual cleanup happens here
}
}
fn main() {
let _conn = DbConnection {
url: "postgres://localhost".to_string(),
};
println!("Connection created");
} // drop is called here: "Closing connection to..."
This pattern is critical in production systems. You never need to manually free memory or close handles. The compiler ensures cleanup happens in the right order, preventing resource leaks that plague C++ codebases.
Copy types—Integers and other simple types are copied, not moved
Not everything moves. Small, stack-allocated types like integers, booleans, and floats implement the Copy trait. When you assign them, they're copied, not moved:
fn main() {
let x = 42;
let y = x; // x is copied, not moved
println!("x: {}, y: {}", x, y); // Both OK
}
You can use x after assigning it to y because Copy types are duplicated rather than transferred. This makes sense for a simple integer—copying 8 bytes is cheap.
The rule: if a type is Copy, assignment copies it. Otherwise, assignment moves it. Standard Copy types include:
- Integers:
i32,u64, etc. - Floats:
f32,f64 - Booleans:
bool - Characters:
char - Tuples of
Copytypes:(i32, i32)
Heap-allocated types like String, Vec<T>, and HashMap<K, V> are not Copy—they move:
fn main() {
let nums = vec![1, 2, 3]; // Vec is not Copy
let nums_copy = nums; // nums is moved
// println!("{:?}", nums); // ERROR: value used after move
println!("{:?}", nums_copy); // OK
}
In a trading system or any high-frequency application, you'd want to avoid moving large collections unnecessarily. That's where references come in—but that's the topic for the next day in this series.
Common mistakes
Mistake 1: Trying to use a value after moving it
let order = String::from("order_123");
process(order);
println!("{}", order); // ERROR: borrow of moved value
Fix: Either use a reference (we'll cover this) or restructure to use the value before passing it away.
Mistake 2: Forgetting that function parameters take ownership
fn log_data(data: Vec<u64>) {
println!("{:?}", data);
}
fn main() {
let my_data = vec![1, 2, 3];
log_data(my_data);
println!("{:?}", my_data); // ERROR: moved value
}
Fix: Pass a reference &Vec<u64> instead if you need to keep using my_data.
Mistake 3: Assuming Copy semantics for all simple types
Even though tuples can be Copy, if they contain non-Copy types, they're not Copy:
let data = (String::from("hello"), 42);
let copy = data;
// println!("{:?}", data); // ERROR: String is not Copy
Key takeaways
- Every value has exactly one owner; ownership can be transferred through assignment or function calls.
- When a value is moved, the previous owner can no longer use it—the compiler enforces this at compile time.
- Values are automatically freed (via the
Droptrait) when their owner goes out of scope, eliminating manual memory management. Copytypes (integers, booleans, floats) are duplicated on assignment; non-Copytypes (strings, vectors, collections) are moved.- Understanding ownership prevents use-after-free bugs, double-frees, and data races before code reaches production.
- When you need to share a value without moving it, use references (covered in the next day's guide).






Comments
Loading comments…