When your struct points outside itself
You are writing a parser that pulls quotes from a novel. The novel is a massive String sitting in memory. You want to store each quote in a struct so you can pass it around your application. Copying every quote into a new String wastes memory and slows down allocation. You want the struct to hold a reference to the original text instead. The compiler immediately stops you. It refuses to compile a struct that contains references without explicit lifetime annotations. This is not a bug. It is a safety feature forcing you to declare how long those references will stay valid.
The lease agreement pattern
A struct with a lifetime parameter is a container that borrows data from somewhere else. The lifetime annotation is a contract between the struct and the compiler. It guarantees that the borrowed data will not be dropped while the struct still exists.
Think of it like a museum placard. The placard holds a description and points to an artifact in another wing. The placard is useless if the artifact gets shipped out or destroyed. The lifetime parameter is the lease term on that artifact. As long as the placard is on display, the lease guarantees the artifact stays in the wing. When the placard is taken down, the lease expires and the artifact can move.
In Rust, you write this contract using a lifetime parameter, usually named 'a. The compiler uses 'a to track the validity window of every reference inside the struct. If any reference expires before the struct does, the compiler rejects the code. If the struct outlives the data, the compiler rejects the code. The rule is strict because a dangling reference causes memory corruption at runtime. Rust moves that risk to compile time.
The basic syntax
You declare the lifetime on the struct definition itself, then attach it to every reference field.
/// Holds a reference to a text excerpt.
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
// The source string lives in this scope.
let novel = String::from("Call me Ishmael.");
// We slice a reference out of it.
let first_sentence = novel.split('.').next().unwrap();
// The struct borrows from first_sentence.
let excerpt = ImportantExcerpt {
part: first_sentence,
};
// We can read the borrowed data safely.
println!("Excerpt: {}", excerpt.part);
}
The 'a on ImportantExcerpt<'a> is a type parameter, just like T in Vec<T>. It does not allocate memory. It does not change the size of the struct. It is purely a compile-time label. The compiler uses it to tie the lifetime of part to the lifetime of the ImportantExcerpt instance. When excerpt goes out of scope, the compiler knows it can safely stop tracking the reference. When novel goes out of scope, the compiler ensures no ImportantExcerpt instances are still alive.
Why elision rules stay out of structs
Functions get lifetime elision. The compiler can guess that a function returning a reference borrows from one of its arguments. Structs get no such mercy. The compiler cannot guess how long a struct should live relative to its fields. A struct might be stored in a global cache, passed to a thread, or dropped immediately. Without an explicit parameter, the compiler has no way to verify that the struct will not outlive the data it points to. You must always write the lifetime parameter on structs that hold references. This explicitness is what makes the type system sound.
What the compiler actually checks
When you compile this code, the borrow checker builds a graph of scopes and references. It sees novel created at the top of main. It sees first_sentence created next. It sees excerpt created after that. The lifetime 'a forces the compiler to verify that excerpt cannot outlive first_sentence, and first_sentence cannot outlive novel.
If you rearrange the scopes, the compiler catches the violation immediately.
fn broken_example() {
let excerpt;
{
let novel = String::from("Call me Ishmael.");
let first_sentence = novel.split('.').next().unwrap();
// This assignment tries to make excerpt outlive the inner block.
excerpt = ImportantExcerpt { part: first_sentence };
} // novel and first_sentence are dropped here.
// excerpt still exists, but its reference points to freed memory.
println!("{}", excerpt.part);
}
The compiler rejects this with E0597 (borrowed value does not live long enough). It points directly to first_sentence and explains that the reference must be valid for the lifetime of excerpt. The error is precise because the lifetime parameter gave the compiler a concrete variable to track. Without 'a, the compiler would have to guess how long the reference should last, and guessing is how memory safety breaks.
A practical pattern: the document cache
Lifetime-annotated structs shine when you are building views over larger datasets. You do not want to duplicate data. You want lightweight handles that point to the source of truth.
Consider a simple search index that stores references to lines in a configuration file. The file is loaded once at startup. The index holds references to specific lines. The index lives as long as the application runs, but it never owns the text.
/// A lightweight index pointing to lines in a larger document.
struct LineIndex<'doc> {
/// The full document text that owns the underlying memory.
document: &'doc str,
/// A list of byte ranges pointing into `document`.
highlights: Vec<(usize, usize)>,
}
impl<'doc> LineIndex<'doc> {
/// Creates a new index over the provided document.
fn new(doc: &'doc str) -> Self {
LineIndex {
document: doc,
highlights: Vec::new(),
}
}
/// Adds a highlight by storing byte offsets.
fn add_highlight(&mut self, start: usize, end: usize) {
self.highlights.push((start, end));
}
/// Returns the highlighted text slice.
fn get_highlight(&self, index: usize) -> Option<&str> {
let (start, end) = self.highlights.get(index)?;
// We slice directly from the owned document reference.
self.document.get(*start..*end)
}
}
fn main() {
let config = String::from("host=localhost\nport=8080\ndebug=true");
let mut index = LineIndex::new(&config);
index.add_highlight(0, 15);
if let Some(line) = index.get_highlight(0) {
println!("Found: {}", line);
}
}
Notice how document carries the 'doc lifetime. The impl block also declares <'doc> so the methods can use it. The highlights field stores usize offsets instead of &str slices. This is a common convention. Storing multiple &str slices with different lifetimes forces you to write struct LineIndex<'a, 'b, 'c>, which quickly becomes unmanageable. Byte offsets or indices keep the struct to a single lifetime parameter while still avoiding data duplication.
Common traps and how to avoid them
Lifetime parameters are easy to misuse when you are not tracking what actually owns the data. The most frequent mistake is attaching a lifetime to a struct that only holds owned values.
// This compiles, but the lifetime serves no purpose.
struct Point<'a> {
x: i32,
y: i32,
}
The compiler allows it, but it adds noise. You only need a lifetime parameter when the struct contains at least one reference, a trait object, or another type that carries a lifetime. If every field is owned, drop the parameter. Keep the API clean.
Another trap is mixing lifetimes incorrectly across fields. If a struct holds two references from different sources, they must share the same lifetime parameter unless you explicitly split them.
struct Mixed<'a, 'b> {
title: &'a str,
body: &'b str,
}
This works, but it forces callers to satisfy both 'a and 'b. In practice, most structs stick to a single lifetime because the data usually comes from the same source. If you find yourself writing <'a, 'b, 'c>, step back and check whether you are overcomplicating the ownership model. Often, a single Rc or Arc solves the problem with less boilerplate.
You will also hit E0308 (mismatched types) when the compiler cannot unify lifetimes. This happens when you try to assign a reference with a shorter lifetime to a struct field expecting a longer one. The fix is almost always to extend the scope of the borrowed data, or to change the struct to own the data instead.
When to reach for lifetime-annotated structs
Use lifetime-annotated structs when you are building a view over existing data and want zero allocation overhead. Use lifetime-annotated structs when the referenced data lives longer than the struct itself, such as configuration loaded at startup or a database row parsed from a buffer. Use lifetime-annotated structs when you are implementing a parser, a cache, or a UI component that reads from a shared document.
Reach for owned structs when the data is small, frequently mutated, or created independently of any larger container. Reach for Rc<T> or Arc<T> when multiple owners need to share the data and you cannot guarantee a single clear lifetime. Reach for indices or byte offsets when you need to store many references inside one struct without multiplying lifetime parameters.
Naming and performance reality
The community convention is to name lifetimes after their source when they carry meaning. 'doc, 'ctx, 'input are clearer than 'a in public APIs. Inside a single function or a small module, 'a is fine. The name does not affect performance. Lifetime parameters are erased at compile time. They do not exist in the binary. The struct layout is identical whether you write struct Excerpt<'a> or struct Excerpt.
You do not need to manually manage these lifetimes. The compiler tracks them automatically. Your job is to declare the relationship, then let the borrow checker enforce it. If the compiler complains, the relationship is broken. Fix the scope or change the ownership model. Do not fight the annotation.
Treat the lifetime parameter as a contract, not a constraint. It exists to keep your references honest. Write it once, and the compiler handles the rest.