Karya Semi
HomeBlogSearchCategoriesAboutContact
Karya Semi

Less noise. More notes.

HomeBlogAboutContactPrivacy PolicyDisclaimer

© 2026 Karya Semi. All rights reserved.

XGitHubLinkedIn
  1. Home
  2. /Categories
  3. /Programming

Rust Ownership and the Borrow Checker: A Practical Mental Model for C++ and Java Developers

Rust's borrow checker feels like a wall until you stop fighting it and start reading what it actually wants. Here's the mental model that finally makes it click.

Dian Rijal Asyrof/June 30, 2026/6 min read
Illustration for Rust Ownership and the Borrow Checker: A Practical Mental Model for C++ and Java Developers

The first time a C++ developer meets Rust, they usually spend an hour furious at the compiler. The code looks fine. It compiled fine in their head. There's even a comment explaining what they meant. The Rust compiler rejects it with a forty-line error about lifetimes, and the developer walks away convinced Rust was designed by someone who hates them.

I spent that hour too. Then I spent about six months occasionally picking Rust back up, hitting the same wall, and walking away again. What finally broke through wasn't more documentation. It was reading the borrow checker as a real constraint solver instead of a pedantic teacher.

This is the model that eventually worked for me, and that I've watched work for other developers coming from C++, Java, or Go. It's not the official mental model you'd find in the Rust book, but it's the one that survived real projects.

The single rule, written before everything else

Every value in Rust has one owner. There is exactly one variable that owns it. When that variable goes out of scope, the value is dropped. That's it.

This rule alone would be useless. The single rule that decides every other one in this model is borrowing. A borrow is a temporary, read-only or read-write reference to a value owned by something else.

The rules around borrows look arbitrary until you write them down together:

  • You can have any number of immutable borrows at the same time.
  • You can have exactly one mutable borrow at the same time.
  • You cannot mix immutable and mutable borrows at the same time.

That's the whole borrow checker. Every cryptic error you'll ever see comes from one of these three rules being violated somewhere.

Why this is more than aesthetics

In a C or C++ program, the rule "you can't read and write to memory at the same time without synchronization" is enforced by you. If you forget, the compiler accepts the code, and at runtime you get a data race, a use-after-free, or undefined behavior that's only reproducible three days before a release.

In Java, the rule "you can't read and write to the same object without locking" is enforced by the runtime via the memory model and locks. The cost is GC pauses, lock contention, and a class of subtle correctness bugs around happens-before ordering. It's safer than C++, but the performance profile is determined by what the JVM decides to do.

In Rust, the rule is enforced at compile time. The compiler proves the invariant statically. If it compiles, no two threads can mutate the same memory without synchronization, and no one can read memory that the owner has dropped. No runtime cost, no GC, no undefined behavior. The cost is the moment of confusion when you're learning the model and the compiler rejects reasonable-looking code.

This is why developers who already read programming error messages well have an easier time with Rust. The borrow checker errors are precise once you learn to read them.

The three kinds of "this is why your code doesn't compile"

Most Rust compile failures in the early learning phase come from one of these three patterns.

Pattern 1: you're holding onto a borrow after the owner moved.

You pass a string into a function that takes ownership. After the call, you try to use the original variable. The compiler says the value was moved. The fix is either to pass a reference instead, or to clone the value before passing it.

Pattern 2: you're trying to mutate while someone else is reading.

You have a Vec<T> and you iterate over &item while another part of the code tries to push to the same vector. The compiler says you have an immutable borrow and a mutable borrow at the same time. The fix is usually to collect the iterator into a vector of references first, then mutate.

Pattern 3: you're returning a reference to something that goes out of scope.

You build a String inside a function, take a reference to it, and return that reference. The compiler says the value doesn't live long enough. The fix is either to return the owned String itself, or to take a reference to something the caller already owns.

These three patterns cover about 80% of the early learning failures. The remaining 20% are real lifetime annotations that you only meet once you start writing code with structs that hold references to other structs.

A worked example that maps to C++ intuition

Suppose you have a function in C++ that takes a const std::vector<int>& and returns a size_t:

size_t count(const std::vector<int>& items) {
    return items.size();
}

In Rust, the idiomatic version is:

fn count(items: &[i32]) -> usize {
    items.len()
}

The &[i32] is a slice, which is a reference to a contiguous slice of memory plus a length. It's the closest thing Rust has to a const std::vector<int>&. It borrows from the caller, doesn't own anything, and the caller is responsible for keeping the underlying memory alive during the borrow.

Now the interesting case. Suppose you want the function to filter the slice and return a new vector:

std::vector<int> positives(const std::vector<int>& items) {
    std::vector<int> out;
    for (int x : items) if (x > 0) out.push_back(x);
    return out;
}

In Rust:

fn positives(items: &[i32]) -> Vec<i32> {
    items.iter().filter(|&&x| x > 0).copied().collect()
}

The borrow is read-only, the iterator takes one item at a time, and you collect into a fresh Vec<i32> that the function owns and returns. No clones of the input, no shared mutable state. The original vector is unchanged, and the caller still has it.

Once you see this pattern a few times, the borrow checker starts feeling less like a wall and more like an editor asking, "but who owns this thing?"

Lifetimes are how the compiler talks about scope

Lifetimes show up when a struct holds a reference to another value. A common case:

struct Excerpt<'a> {
    text: &'a str,
}

The 'a annotation is a lifetime parameter. It's saying: this struct holds a reference to a str that lives at least as long as the struct itself. The compiler uses this information to verify that the reference can't outlive the data it points to.

In practice, most Rust code doesn't make you write these annotations. The compiler infers them. You only write lifetimes explicitly when the compiler can't figure out the relationships by itself, which happens mostly when you have functions that return references or when you have multiple input references that could each be the source.

What you should and shouldn't fight

The borrow checker is right more often than you think. When you find yourself wanting to use unsafe or Rc<RefCell<T>> to bypass a borrow error, slow down and ask why the compiler is objecting.

Common cases where the compiler is wrong (use Cell or interior mutability):

  • Building a cache that is read often and written rarely.
  • Implementing graph data structures with shared nodes.
  • Building a self-referential struct (rare, usually means you want to redesign the data).

Common cases where the compiler is right (redesign):

  • A function takes &mut self and also wants to borrow another field of the same struct.
  • A long-lived iterator that holds a borrow across an operation that should be allowed to mutate.
  • A callback that captures a mutable reference to something the caller still uses.

The mental shift is from "how do I trick the compiler into accepting this" to "what data structure makes this constraint obvious." Once that shift happens, the language becomes pleasant to work with.

Practical Rust for a polyglot codebase

For teams running mixed codebases, the practical path is to keep new services in Rust and let variable naming follow the language when the language changes. Tooling-wise, the Rust ecosystem is mature enough now that you can run cargo alongside pip, npm, and go modules without friction.

The places Rust is genuinely worth the investment in 2026:

  • Anything where memory safety matters and you don't want a GC pause. Network proxies, databases, parsers, command-line tools.
  • Anything that touches untrusted input at the syscall boundary. The borrow checker rules out whole classes of memory corruption bugs.
  • Performance-critical paths that have to scale to many cores.

The places Rust is still painful:

  • Quick internal tools where Python or TypeScript takes a tenth of the time.
  • Frontend. WASM toolchains are improving but the developer experience still trails JavaScript.
  • Anywhere your team's learning curve would dominate the project schedule.

Rust isn't replacing C++ or Java in the next five years. It is becoming the default choice for new infrastructure code, and the borrow checker is the reason. Once you internalize the model, it's not a wall. It's a checkpoint that catches the bugs you'd otherwise ship at 3 AM.

DR

Dian Rijal Asyrof

Writes about useful AI tools, programming practice, and the craft of building reliable software.

Previous articleARM in the Data Center: Why Your Cloud Bill Looks Different in 2026
ProgrammingRustMemory SafetyDebugging
On this page↓
  1. The single rule, written before everything else
  2. Why this is more than aesthetics
  3. The three kinds of "this is why your code doesn't compile"
  4. A worked example that maps to C++ intuition
  5. Lifetimes are how the compiler talks about scope
  6. What you should and shouldn't fight
  7. Practical Rust for a polyglot codebase

On this page

  1. The single rule, written before everything else
  2. Why this is more than aesthetics
  3. The three kinds of "this is why your code doesn't compile"
  4. A worked example that maps to C++ intuition
  5. Lifetimes are how the compiler talks about scope
  6. What you should and shouldn't fight
  7. Practical Rust for a polyglot codebase

See also

Illustration for How to Read Programming Error Messages Without Panic
Programming/Jun 28, 2026

How to Read Programming Error Messages Without Panic

Learn how to read programming error messages, find the useful line in a stack trace, translate common errors, and debug one small clue at a time.

5 min read
DebuggingError Messages
Illustration for Git 2.55 Quietly Fixes How Massive Repos Stay Maintainable
Programming/Jun 30, 2026

Git 2.55 Quietly Fixes How Massive Repos Stay Maintainable

Git 2.55 brings incremental MIDX repacking, reftable improvements, and faster performance on huge repositories. Here's what changed.

3 min read
ProgrammingGit
Illustration for Variable Naming Best Practices for Readable Code
Programming/Jun 28, 2026

Variable Naming Best Practices for Readable Code

Variable naming best practices for writing readable code, from clear nouns and boolean names to function verbs, scope, and safe refactoring habits.

4 min read
Variable NamingClean Code