Introduction
Ownership is Rust's most famous feature, but borrowing is what makes it practical. Without the ability to borrow, every function that reads data would force you to move it, leaving the caller unable to use the value afterward. This guide on borrowing and references: a hands-on guide for engineers covers shared references, mutable references, and the fundamental rule that keeps Rust's memory safety guarantees intact—rules we'll apply to real production scenarios you've likely encountered in e-commerce, APIs, and data processing pipelines.
Shared references — &T — many readers allowed
A shared reference, written as &T, lets you read a value without taking ownership. Multiple shared references can exist at the same time, making them ideal when you need to pass data to several functions or threads without moving it around.
Think of shared references like handing out photocopies of a document. Everyone gets a copy, everyone can read it, but no one can mark it up. The original owner keeps the document.
Here's the canonical example:
fn len(s: &String) -> usize {
s.len()
}
fn main() {
let name = String::from("Expona");
let n = len(&name);
println!("Length: {}", n);
println!("Name: {}", name); // still usable
}The function len takes a reference &String instead of ownership. Inside main, we call it with &name—the & operator creates a reference. After the call, name is still valid and we can use it again.
In a real service, this pattern appears everywhere:
// In a CRM system, fetch customer details
fn validate_email(email: &str) -> bool {
email.contains('@')
}
fn log_signup(email: &str, username: &str) {
println!("User {} signed up with {}", username, email);
}
fn main() {
let user_email = String::from("alice@example.com");
let user_name = String::from("alice");
if validate_email(&user_email) {
log_signup(&user_email, &user_name);
// Both strings still owned by main, can continue using them
}
}Because both functions borrow instead of take ownership, main retains control over the data. No cloning needed.
Mutable references — &mut T — exactly one writer at a time
A mutable reference, written as &mut T, lets you modify the borrowed value. But there's a strict rule: only one mutable reference can exist at a time for a given value. This prevents data races and unexpected mutations.
Imagine a shared spreadsheet. One person can edit it exclusively at a time. While they're editing, no one else—not even readers—can see the intermediate states.
Basic example:
fn increment(counter: &mut i32) {
*counter += 1; // dereference and mutate
}
fn main() {
let mut count = 0;
increment(&mut count);
println!("Count: {}", count); // 1
}The &mut count borrows count mutably. Inside increment, we use the dereference operator * to access and modify the value. After the borrow ends, main regains exclusive access.
In a trading system, mutable borrows are essential for safe state updates:
struct Portfolio {
cash: f64,
shares: i32,
}
fn buy(portfolio: &mut Portfolio, share_price: f64) {
if portfolio.cash >= share_price {
portfolio.cash -= share_price;
portfolio.shares += 1;
}
}
fn main() {
let mut my_portfolio = Portfolio { cash: 1000.0, shares: 0 };
buy(&mut my_portfolio, 250.0);
println!("Cash: {}, Shares: {}", my_portfolio.cash, my_portfolio.shares);
}The &mut my_portfolio borrow ensures only buy modifies the portfolio during the call. No concurrent mutations can happen accidentally.
The rule — Either many readers or one writer, never both
This is Rust's core borrowing rule, enforced at compile time. At any point in your program, you can have either:
- Many immutable (shared) references to a value, or
- Exactly one mutable reference to a value
You cannot have both at the same time. This eliminates entire classes of bugs—use-after-free, data races, iterator invalidation—without runtime overhead.
The compiler tracks the scope of each reference. Once a reference is no longer used, its borrow ends and the value becomes available again.
Here's where the rule prevents bugs:
fn main() {
let mut data = vec![1, 2, 3];
let r1 = &data; // immutable borrow
let r2 = &data; // another immutable borrow, OK
println!("{}, {}", r1, r2);
// r1 and r2 are no longer used after the println
let w = &mut data; // mutable borrow, OK now
w.push(4);
println!("{:?}", w);
}If you tried to use r1 or r2 after the mutable borrow began, the compiler would reject it. Borrows have scopes, and the compiler is precise about them.
Compare to a violation:
// This does NOT compile
fn main() {
let mut data = vec![1, 2, 3];
let r1 = &data;
let w = &mut data; // ERROR: cannot borrow as mutable
println!("{}", r1); // r1 still in use
}The compiler sees that r1 is in scope and might be used later, so it forbids the mutable borrow. This is a compile-time check, not a runtime panic.
Example — Borrow instead of moving
Let's build a more realistic scenario: processing customer orders in an e-commerce system.
struct Order {
id: u64,
customer_name: String,
amount: f64,
}
fn process_payment(order: &Order) -> bool {
println!("Processing payment for {} (${:.2})", order.customer_name, order.amount);
order.amount > 0.0
}
fn send_confirmation_email(order: &Order) {
println!("Email sent to {}", order.customer_name);
}
fn log_order(order: &Order) {
println!("Logged order {}", order.id);
}
fn main() {
let order = Order {
id: 12345,
customer_name: String::from("Alice"),
amount: 99.99,
};
if process_payment(&order) {
send_confirmation_email(&order);
log_order(&order);
}
// order is still ours and valid
println!("Final order: {} - {}", order.id, order.customer_name);
}Without borrowing, you'd have to clone or move the order around, losing ownership and requiring defensive copies. With shared references, every function borrows what it needs and returns control. The business logic stays clean.
Common mistakes
Forgetting the & operator: Many engineers reach for values directly and then wonder why the borrow checker complains. Remember: if a function expects &T, you pass &value.
fn takes_ref(x: &i32) {}
fn main() {
let num = 42;
takes_ref(num); // ERROR: expected &i32, found i32
takes_ref(&num); // Correct
}Mixing mutable and immutable borrows carelessly: If you hold an immutable borrow, you cannot create a mutable one in the same scope before the immutable one is dropped.
fn main() {
let mut x = 5;
let r1 = &x;
let r2 = &mut x; // ERROR: cannot borrow as mutable while immutable borrow is active
}Assuming dereference is automatic: When you have a mutable reference and need to modify the value it points to, use * to dereference. Rust won't do it for you in all cases.
fn increment(x: &mut i32) {
*x += 1; // necessary dereference
}Over-borrowing in loops: Creating a borrow at the start of a loop scope can unexpectedly extend its lifetime. Be explicit about scopes if needed.
fn main() {
let mut list = vec![1, 2, 3];
for _ in 0..2 {
let r = &list;
println!("{:?}", r);
// r's borrow ends here, within the loop iteration
}
list.push(4); // OK, no active borrows
}Key takeaways
- Shared references
&Tlet multiple readers access data without moving ownership. - Mutable references
&mut Tallow one writer at a time, preventing data races and unexpected mutations. - The borrowing rule—either many immutable or one mutable borrow, never both—is enforced at compile time and eliminates entire categories of bugs.
- Borrows are scoped. Once a reference is no longer used, the borrow ends and the value becomes available again.
- Always pass references when you don't need ownership; this keeps APIs flexible and data flowing through your codebase without unnecessary moves.
- In production systems (CRM, trading, e-commerce), borrowing replaces manual lifetime management and defensive cloning with compiler-verified safety.






Comments
Loading comments…