What borrow splitting solves

A classic example is trying to take two mutable references from the same collection:

fn swap_bad(v: &mut Vec<i32>, i: usize, j: usize) {
    let a = &mut v[i];
    let b = &mut v[j];
    std::mem::swap(a, b);
}

This fails because the compiler cannot assume i != j. Even if you know the indices are different, the borrow checker needs a stronger guarantee than “trust me.”

Borrow splitting gives you a way to express that guarantee explicitly, usually by:

  • splitting a slice into two non-overlapping slices,
  • destructuring a struct into separate field borrows,
  • using APIs that return disjoint mutable references,
  • or designing your own safe abstractions around internal partitioning.

The core idea: prove disjointness

Rust allows multiple mutable references only when they cannot alias. The compiler already understands some forms of disjointness:

  • different struct fields,
  • separate elements from a split slice,
  • distinct values returned by certain standard library methods.

The key is to structure your code so the compiler can verify separation at the type level or through API boundaries.

Struct fields are independently borrowable

If you have a struct, you can borrow different fields mutably at the same time:

struct Player {
    health: i32,
    mana: i32,
}

fn heal_and_restore(player: &mut Player) {
    let health = &mut player.health;
    let mana = &mut player.mana;

    *health += 10;
    *mana += 5;
}

This works because health and mana are distinct fields. The compiler knows they do not overlap.

This pattern is ideal when your data model can be expressed as a struct with meaningful subfields. Prefer this over packing everything into a single container when you expect frequent independent mutation.


Splitting slices with the standard library

For contiguous collections, the standard library provides safe splitting APIs. The most common is split_at_mut.

fn swap_in_slice(v: &mut [i32], i: usize, j: usize) {
    assert!(i < v.len() && j < v.len());
    assert!(i != j);

    let (left, right) = if i < j {
        let (l, r) = v.split_at_mut(j);
        (l, r)
    } else {
        let (l, r) = v.split_at_mut(i);
        (r, l)
    };

    if i < j {
        std::mem::swap(&mut left[i], &mut right[0]);
    } else {
        std::mem::swap(&mut left[0], &mut right[j]);
    }
}

This example is a bit awkward because we are manually mapping indices into the split halves. A cleaner approach is to use split_at_mut only when you need two known regions, not arbitrary element access.

A more practical example:

fn add_offset_to_tail(v: &mut [i32], split: usize, offset: i32) {
    let (head, tail) = v.split_at_mut(split);

    for x in head {
        *x += offset;
    }

    for x in tail {
        *x -= offset;
    }
}

Here, head and tail are guaranteed disjoint, so both can be mutated independently.

Why split_at_mut matters

Without it, you might be tempted to use raw pointers or clone data. split_at_mut gives you:

  • zero-copy access,
  • compile-time safety,
  • clear intent,
  • and no unsafe code in your application logic.

Borrow splitting in real APIs

A good API often makes borrow splitting feel natural. Consider a text editor buffer that stores the document and a selection range. You may want to mutate the text while also updating cursor metadata.

struct Buffer {
    text: String,
    cursor: usize,
    dirty: bool,
}

impl Buffer {
    fn insert_char(&mut self, ch: char) {
        self.text.insert(self.cursor, ch);
        self.cursor += ch.len_utf8();
        self.dirty = true;
    }
}

This method works because it mutably accesses fields one at a time through a single &mut self. But if you need to hand out separate mutable references to internal components, you should design the API carefully.

Returning multiple mutable references

You can expose a method that returns disjoint borrows of fields:

struct Config {
    network_timeout_ms: u64,
    retry_count: u32,
}

impl Config {
    fn parts_mut(&mut self) -> (&mut u64, &mut u32) {
        (&mut self.network_timeout_ms, &mut self.retry_count)
    }
}

This is safe because the fields are distinct. It is also a strong signal to callers that the two values can be updated independently.


When the compiler cannot infer disjointness

Sometimes the data layout is safe, but the compiler cannot prove it from your code. This usually happens with indexing, loops, or conditional access.

Example: updating two elements by index

Suppose you want to mutate two entries in a vector:

fn increment_pair(v: &mut [i32], a: usize, b: usize) {
    assert!(a != b);
    let (x, y) = if a < b {
        let (left, right) = v.split_at_mut(b);
        (&mut left[a], &mut right[0])
    } else {
        let (left, right) = v.split_at_mut(a);
        (&mut right[0], &mut left[b])
    };

    *x += 1;
    *y += 1;
}

This pattern is common, but it can be simplified by using helper functions or by restructuring the algorithm to operate on partitions rather than arbitrary pairs.

Prefer partition-oriented design

If your algorithm repeatedly needs disjoint mutable access, consider organizing data so that the partitions are explicit:

SituationBetter approach
Mutating two fields of one objectUse a struct and borrow fields separately
Mutating two halves of a bufferUse split_at_mut
Mutating a subset and the restPartition the data first
Mutating arbitrary pairs oftenConsider a map of independent nodes or indices

The more your data model reflects the access pattern, the less borrow-checker friction you will encounter.


Borrow splitting with custom data structures

If you implement your own container, you may need to provide safe APIs that return multiple mutable references. The challenge is to ensure the references never overlap.

A common pattern is to split by index range.

struct RingBuffer<T> {
    data: Vec<T>,
}

impl<T> RingBuffer<T> {
    fn split_mut(&mut self, mid: usize) -> (&mut [T], &mut [T]) {
        self.data.split_at_mut(mid)
    }
}

This wrapper is trivial, but it illustrates an important principle: if the standard library already provides a safe splitting primitive, reuse it instead of reimplementing it.

Designing safe accessors

For more complex structures, such as trees or arenas, safe splitting often requires:

  • validating indices or node IDs,
  • proving that two IDs refer to different nodes,
  • or traversing to distinct subtrees.

A good API should encode these rules in its return type or method contract. For example, a tree might expose:

  • left_mut() and right_mut() for binary nodes,
  • get_two_mut(id1, id2) for distinct nodes,
  • or iterators that yield non-overlapping mutable references in a controlled order.

Common pitfalls

1. Holding a mutable borrow longer than necessary

A borrow can block later access even if the code is logically safe. Keep borrows scoped tightly.

fn update(buffer: &mut Buffer) {
    {
        let text = &mut buffer.text;
        text.push_str("!");
    }

    buffer.dirty = true;
}

The inner block ends the borrow before buffer.dirty is modified. This is often enough to satisfy the compiler without changing the design.

2. Borrowing through indexing repeatedly

Repeated indexing can create overlapping borrow attempts. Instead, split once and reuse the resulting references.

3. Overusing clone

Cloning to avoid borrow issues is sometimes acceptable, but it can hide design problems and add unnecessary cost. Prefer structural solutions first.

4. Reaching for unsafe too early

Unsafe code can implement borrow splitting, but it should be the last resort. Most cases are solved by:

  • split_at_mut,
  • field destructuring,
  • Option::take,
  • or redesigning the API around ownership.

A practical pattern: temporarily taking ownership

Sometimes you need to mutate one field while also calling methods that require &mut self. In that case, moving a field out temporarily can help.

struct Session {
    cache: Option<String>,
    counter: u64,
}

impl Session {
    fn refresh(&mut self) {
        let cache = self.cache.take();

        self.counter += 1;

        self.cache = Some(cache.unwrap_or_default());
    }
}

Option::take replaces the field with None and returns the old value, letting you work on it independently. This is a form of borrow splitting by ownership transfer.

Use this pattern when a field is logically optional during an operation and you need to avoid aliasing self.


Choosing the right technique

Here is a practical summary of common borrow-splitting tools:

TechniqueBest forNotes
Struct field borrowsIndependent fieldsSimplest and most ergonomic
split_at_mutSlices and contiguous buffersSafe, zero-cost, widely applicable
Scoped borrowsShort-lived accessOften enough to satisfy the compiler
Option::takeTemporarily moving out a fieldUseful for reentrant or staged updates
Custom safe accessorsTrees, arenas, graphsRequires careful API design

The best choice is usually the one that makes the disjointness obvious to both the compiler and future readers.


Best practices for production code

Keep partitions explicit

If a function depends on two disjoint regions, name them accordingly: left, right, head, tail, active, inactive. Clear naming reduces mistakes and makes invariants easier to audit.

Prefer safe primitives from the standard library

Before writing custom logic, check whether split_at_mut, get_many_mut-style APIs, or ownership transfers already solve the problem.

Design around access patterns

If your code frequently needs simultaneous mutable access to separate parts, that is often a sign your data structure should be reorganized.

Minimize borrow scope

Short borrows are easier to reason about and often eliminate compiler errors without any algorithmic changes.

Document invariants in APIs

If a method returns multiple mutable references, document the disjointness guarantee clearly. Callers should understand why the API is safe and when it is valid to use.


Conclusion

Borrow splitting is one of Rust’s most practical advanced techniques. It lets you preserve safety while still writing efficient code that mutates multiple parts of a structure at once. The main idea is simple: make disjointness explicit, either through data layout, standard library primitives, or carefully designed APIs.

When you align your types with your access patterns, the borrow checker becomes an ally rather than an obstacle.

Learn more with useful resources