Lifetimes

Practical Lifetime Design

Practical lifetime design is the discipline of structuring data types, function signatures, and ownership boundaries so that code remains ergonomic, avoids unnecessary lifetime annotations, and uses zero-copy borrows only where they provide measurable benefit.

Choosing Between Owned Data and Borrowed References in Structs

Every 'a parameter added to a struct propagates to its impl blocks, functions that accept or return it, and parent structs that contain it. Before adding a lifetime parameter to a struct, evaluate how the struct will be used across your program.

Design Scenario Recommended Field Type Reason
Long-lived application state, configuration, or domain entities Owned (String, Vec<T>) Can be moved freely across threads, stored in collections, and returned from functions without lifetime propagation
Short-lived zero-copy parser, tokenizer, or view over an existing buffer Borrowed (&'a str, &'a [u8]) Avoids heap allocations while scanning a single input buffer that outlives the view
Data that is mostly borrowed but occasionally modified or synthesized Clone-on-write (Cow<'a, str>, Cow<'a, [T]>) Borrows without allocation in the common path and allocates an owned copy only when mutation is required
Immutable data shared across multiple long-lived owners or tasks Reference-counted (Arc<str>, Rc<[T]>) Eliminates 'a annotations while avoiding deep copies of large buffers
Lifetime Propagation Cost:
  Borrowed Struct:   Token<'a>  ──>  Lexer<'a>  ──>  Parser<'a>  ──>  AstNode<'a>
  Owned Boundary:    Token<'a>  ──>  Lexer<'a>  ──(to_owned)──>  AstNode (no 'a!)

Strategy 1: Keep Borrowed Views Short-Lived and Convert at Boundaries

A common pattern is to use borrowed slices (&'a str) during parsing or filtering and convert only the extracted results into owned types at the module boundary:

#[derive(Debug, PartialEq)]
struct ConfigEntry {
    key: String,
    value: String,
}

// Short-lived borrowed view used internally during line parsing
struct RawLine<'a> {
    key: &'a str,
    value: &'a str,
}

impl<'a> RawLine<'a> {
    fn parse(line: &'a str) -> Option<Self> {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') {
            return None;
        }
        let (key, value) = line.split_once('=')?;
        Some(Self {
            key: key.trim(),
            value: value.trim(),
        })
    }

    fn into_owned(self) -> ConfigEntry {
        ConfigEntry {
            key: self.key.to_owned(),
            value: self.value.to_owned(),
        }
    }
}

fn load_config(input: &str) -> Vec<ConfigEntry> {
    input
        .lines()
        .filter_map(RawLine::parse)
        .map(RawLine::into_owned)
        .collect()
}

fn main() {
    let raw_text = String::from("# Server config\nhost = 127.0.0.1\nport = 8080\n");
    let entries = load_config(&raw_text);
    drop(raw_text); // `entries` is independent of `raw_text`!

    for entry in &entries {
        println!("{} -> {}", entry.key, entry.value);
    }
}
host -> 127.0.0.1
port -> 8080

Strategy 2: Separate Independent Lifetimes in Structs and Methods

When a struct borrows from an external buffer ('buf), methods on that struct should return references tied to 'buf rather than to the temporary borrow of &self. Tying the return lifetime to 'buf allows callers to keep extracted slices even after the helper struct is mutated or dropped:

struct Cursor<'buf> {
    bytes: &'buf [u8],
    pos: usize,
}

impl<'buf> Cursor<'buf> {
    fn new(bytes: &'buf [u8]) -> Self {
        Self { bytes, pos: 0 }
    }

    // Returns `&'buf [u8]`, NOT a borrow tied to `&mut self`!
    fn take(&mut self, len: usize) -> Option<&'buf [u8]> {
        let end = self.pos.checked_add(len)?;
        if end <= self.bytes.len() {
            let chunk = &self.bytes[self.pos..end];
            self.pos = end;
            Some(chunk)
        } else {
            None
        }
    }
}

fn main() {
    let packet = [0xDE, 0xAD, 0x01, 0x02, 0x03];
    let mut cursor = Cursor::new(&packet);

    // Because `take` returns `&'buf [u8]`, `header` and `payload` can coexist
    // even though `cursor` is mutably borrowed twice in succession:
    let header = cursor.take(2).unwrap();
    let payload = cursor.take(3).unwrap();

    println!("Header:  {header:02X?}");
    println!("Payload: {payload:02X?}");
}
Header:  [DE, AD]
Payload: [01, 02, 03]

Strategy 3: Use Cow<'a, B> for Conditional Allocation

std::borrow::Cow<'a, B> (Clone-on-Write) represents either a borrowed reference (Cow::Borrowed(&'a B)) or an owned value (Cow::Owned(<B as ToOwned>::Owned)). It is ideal when a function usually returns its input slice unchanged and only allocates a new String or Vec<T> when an escape character or invalid byte is encountered:

use std::borrow::Cow;

fn normalize_path(path: &str) -> Cow<'_, str> {
    if path.contains('\\') {
        // Allocates a new `String` only when backslashes are present
        Cow::Owned(path.replace('\\', "/"))
    } else {
        // Zero heap allocation when the path already uses forward slashes
        Cow::Borrowed(path)
    }
}

fn main() {
    let unix_path = "/usr/local/bin";
    let win_path = r"C:\Users\prime\bin";

    let n1 = normalize_path(unix_path);
    let n2 = normalize_path(win_path);

    println!("n1: {n1} (is_borrowed = {})", matches!(n1, Cow::Borrowed(_)));
    println!("n2: {n2} (is_borrowed = {})", matches!(n2, Cow::Borrowed(_)));
}
n1: /usr/local/bin (is_borrowed = true)
n2: C:/Users/prime/bin (is_borrowed = false)

Summary of Lifetime Design Guidelines

  1. Accept borrowed slices in function parameters: Prefer &str over &String and &[T] over &Vec<T>.
  2. Rely on lifetime elision first: Write explicit 'a annotations only when the compiler requires disambiguation or when decoupling 'buf from &self.
  3. Avoid &'a mut T inside structs unless strictly necessary: Mutable references are invariant in T and cannot be copied; prefer passing &mut T to methods or storing indices/offsets instead.
  4. Stop lifetime contagion at subsystem boundaries: Convert transient parser views into owned domain structs or Arc<str> before storing them in long-lived state.