midRust

How does Rust manage memory without a garbage collector? Explain ownership, borrowing, move semantics, and lifetimes with examples.

Updated Feb 20, 2026

Short answer

Rust interview questions typically evaluate understanding of ownership, borrowing, lifetimes, memory safety, concurrency, and idiomatic problem-solving. A mid-level Rust engineer should be able to explain how Rust prevents common memory errors without a garbage collector and how its type system influences design decisions. The specific question is not provided, so the answer below focuses on a common mid-level Rust interview topic: ownership and borrowing.

Deep explanation

Rust's ownership system is the core mechanism that provides memory safety without requiring a garbage collector. Every value in Rust has a single owner, and the compiler enforces rules about how values are moved, borrowed, and destroyed.

The three fundamental ownership rules are:

  1. Each value in Rust has an owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, Rust automatically drops the value.

Example:

Rust
fn main() {
let name = String::from("Rust");
println!("{}", name);
}

Here:

  • name owns the allocated String.
  • When main ends, Rust automatically calls drop.
  • Memory is released deterministically.

---

Move semantics

Rust does not implicitly copy heap-allocated data.

Example:

Rust
fn main() {
let original = String::from("hello");
let moved = original;
println!("{}", moved);
}

After the assignment:

Rust
let moved = original;

ownership moves from original to moved.

This would fail:

Rust
fn main() {
let original = String::from("hello");
let moved = original;
println!("{}", original);
}

Compiler error:

TEXT
value borrowed here after move

The reason is safety.

If Rust allowed both variables to reference the same heap allocation:

TEXT
original ----+
|
v
Heap Data
^
|
moved -------+

both variables could attempt to free the same memory, causing a double free.

---

Copy types

Some simple types implement the Copy trait.

Examples:

  • Integers.
  • Booleans.
  • Characters.
  • Small fixed-size values.

Example:

Rust
fn main() {
let x = 10;
let y = x;
println!("{}", x);
println!("{}", y);
}

This works because x is copied rather than moved.

The difference:

TEXT
String:
Stack:
pointer
length
capacity
Heap:
allocated bytes
Integer:
Stack:
value

Copying a pointer without copying the heap data would be unsafe, so String does not implement Copy.

---

Borrowing

Instead of transferring ownership, Rust allows borrowing references.

Immutable borrow:

Rust
fn print_length(value: &String) {
println!("{}", value.len());
}
fn main() {
let name = String::from("Rust");
print_length(&name);
println!("{}", name);
}

The function receives a reference:

TEXT
name
|
v
String data
^
|
&name

The function can read the data but does not own it.

---

Mutable borrowing

Mutable references allow modification:

Rust
fn add_suffix(value: &mut String) {
value.push_str(" language");
}
fn main() {
let mut name = String::from("Rust");
add_suffix(&mut name);
println!("{}", name);
}

Rules:

  • You can have many immutable references.
  • You can have only one mutable reference.
  • You cannot have immutable and mutable references at the same time.

Valid:

Rust
let a = &value;
let b = &value;

Invalid:

Rust
let a = &value;
let b = &mut value;

The restriction prevents data races.

---

Lifetimes

Lifetimes describe how long references remain valid.

Example:

Rust
fn longest<'a>(
x: &'a str,
y: &'a str
) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}

The lifetime annotation:

Rust
'a

means:

"The returned reference will live at least as long as both input references."

Without this information, Rust cannot guarantee that the returned reference is valid.

---

Why Rust uses ownership instead of garbage collection

Languages with garbage collection:

TEXT
Application
|
v
Runtime GC
|
v
Memory cleanup

Rust uses compile-time analysis:

TEXT
Source Code
|
v
Rust Compiler
|
v
Safe Machine Code

Advantages:

  • No garbage collector pauses.
  • Predictable memory usage.
  • Low runtime overhead.
  • Strong concurrency guarantees.

Trade-offs:

  • Steeper learning curve.
  • More compiler-driven design.
  • More explicit ownership decisions.

---

Ownership and concurrency

Rust's ownership model also prevents many threading bugs.

Example:

Rust
use std::thread;
fn main() {
let message = String::from("hello");
thread::spawn(move || {
println!("{}", message);
})
.join()
.unwrap();
}

The move keyword transfers ownership into the spawned thread.

Without it, Rust would reject the code because the thread could outlive the original scope.

This prevents:

  • Use-after-free.
  • Data races.
  • Invalid references.

---

Traits and ownership-related design

Rust often uses traits to express capabilities.

Example:

Rust
trait Storage {
fn save(&self, data: &str);
}

Implementations decide behavior:

Rust
struct Database;
impl Storage for Database {
fn save(&self, data: &str) {
println!("Saving {}", data);
}
}

Ownership rules influence APIs:

Rust
fn consume(data: String)

means:

  • Caller gives ownership.
Rust
fn read(data: &String)

means:

  • Caller keeps ownership.
Rust
fn modify(data: &mut String)

means:

  • Caller allows controlled mutation.

---

Real-world example

A backend service receives incoming requests and stores user data.

A naive design might copy data unnecessarily:

Rust
fn process_user(name: String) {
println!("{}", name);
}
fn main() {
let username = String::from("alice");
process_user(username);
}

The function takes ownership because it needs to consume the value.

If the application needs to reuse the username:

Rust
fn process_user(name: &str) {
println!("{}", name);
}
fn main() {
let username = String::from("alice");
process_user(&username);
println!("{}", username);
}

The function borrows the string.

A web service might use this pattern:

Rust
struct User {
name: String,
}
fn validate_user(user: &User) -> bool {
!user.name.is_empty()
}
fn save_user(user: User) {
println!("Saving {}", user.name);
}

The validation step borrows the user because it only reads data. The save step takes ownership because it stores or consumes the object.

This design makes ownership intent explicit and prevents accidental memory misuse.

Common mistakes

  • * Assuming Rust uses a garbage collector like Java or Go.
  • * Thinking ownership means values are always copied.
  • * Using `clone()` everywhere to avoid compiler errors.
  • * Confusing references with ownership transfers.
  • * Creating mutable references while immutable references are active.
  • * Ignoring lifetime requirements when returning references.
  • * Fighting the borrow checker instead of redesigning APIs.
  • * Using shared mutable state without synchronization.
  • * Forgetting that threads require ownership-safe data movement.
  • * Assuming Rust guarantees logical correctness, rather than only memory and concurrency safety.

Follow-up questions

  • How does Rust prevent data races at compile time?
  • What is the difference between String and &str in Rust?
  • When would you use clone() in Rust?
  • What are Send and Sync traits used for?
  • How would you design a Rust API to minimize ownership complexity?
  • Why does Rust sometimes require lifetime annotations?

More Rust interview questions

View all →