Learn / Rust Practical Systems
Ownership, Config, and Results
Use borrowing, owned return values, enums, and Result to build small reliable system components.
Course: Rust Practical Systems. Level: Intermediate. Topic: Systems reliability.
Stage: basic - Basic ownership contracts - Rust ownership, config, and Result contracts. Start by making data ownership, configuration parsing, and recoverable errors explicit.
Outcomes
- Borrow read-only inputs.
- Return owned normalized values.
- Use Result for recoverable configuration errors.
- Explain moves, copies, clones, references, mutable references, and slices
- Choose between taking ownership, borrowing immutably, and borrowing mutably
- Understand lifetime annotations as relationships between references
Concepts
- ownership
- borrow
- Result
- enum
- Ownership, Borrowing, and Lifetimes
- Ownership and moves
- References, slices, and mutation
- Lifetimes describe relationships
- Rust for Reliable Systems and Services foundations
- Guided practice
Concept flow
Show how rust ownership, config, and result contracts moves from trigger to implementation outcome in Rust Systems.
- Raw config
- Parser
- Typed config
- Result
- Service startup
Session flow
- Model ownership (concept, 9 min) — Name the decisions behind ownership before writing code.
- Borrow read-only inputs.
- Explain where ownership belongs in job orchestration service.
- Build the vertical slice (walkthrough, 16 min) — Implement the smallest useful slice in src/config.rs.
- Return owned normalized values.
- Connect borrow to a working example.
- Verify and harden (exercise, 11 min) — Add tests for invalid config.
- Use Result for recoverable configuration errors.
- Record one risk or follow-up before moving on.
- Ownership, Borrowing, and Lifetimes: Ownership and moves (concept, 40 min) — Every value has exactly one owner. When the owner goes out of scope, Rust drops the value. Types that own resources, such as String and Vec, move by default. Simple Copy types such as integers can be duplicated without invalidating the original binding.
- Retained source example: Taking ownership versus borrowing
fn length_owned(value: String) -> usize {
value.len()
}
fn length_borrowed(value: &str) -> usize {
value.len()
}
fn main() {
let name = String::from("Ferris");
println!("{}", length_borrowed(&name));
println!("{}", length_owned(name));
// println!("{name}"); // name was moved
}
Expected output: 6
6
Borrowing lets the caller keep ownership. Taking String consumes the caller's value.
- Ownership, Borrowing, and Lifetimes: References, slices, and mutation (walkthrough, 40 min) — At any moment you may have many immutable references or one mutable reference to a value, but not both. This rule prevents iterator invalidation and data races in safe code. Slices are references to contiguous ranges and are often better API parameters than owned collections.
- Retained source example: Mutating through one mutable borrow
fn normalize(items: &mut Vec<String>) {
for item in items {
*item = item.trim().to_lowercase();
}
}
fn main() {
let mut tags = vec![String::from(" Rust "), String::from("API")];
normalize(&mut tags);
println!("{tags:?}");
}
Expected output: ["rust", "api"]
The function borrows the vector mutably long enough to update each String.
- Ownership, Borrowing, and Lifetimes: Lifetimes describe relationships (walkthrough, 40 min) — Most lifetimes are inferred. You write lifetime annotations when a function returns a reference or stores references in structs and Rust needs to know which input lifetime the output depends on. Annotations do not extend lifetimes; they describe valid relationships.
- Retained source example: Returning one of two borrowed values
fn longest<'a>(left: &'a str, right: &'a str) -> &'a str {
if left.len() >= right.len() {
left
} else {
right
}
}
The returned reference is valid only as long as both inputs are valid for the shared lifetime 'a.
- Ownership, Borrowing, and Lifetimes: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Accept &str instead of &String when you only need text.
- Practice: Accept slices such as &[T] instead of &Vec<T> when you only need a sequence.
- Practice: Use clone deliberately at ownership boundaries, and document why the extra allocation is acceptable.
- Avoid: Returning references to local variables that are dropped at function exit.
- Avoid: Using lifetime annotations to try to fix ownership instead of changing data ownership.
- Avoid: Holding a mutable borrow longer than necessary by using a variable outside its real scope.
- Ownership, Borrowing, and Lifetimes: references (review, 2 min) — Original references retained from the legacy library.
- Rust book: ownership: https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html
- Rust book: lifetimes: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html
Code example
Rust in src/config.rs.
fn normalize_slug(input: &str) -> String {
input
.trim()
.to_lowercase()
.split_whitespace()
.collect::<Vec<_>>()
.join("-")
}
Walkthrough examples
- Ownership, Config, and Results in a job orchestration service — A team is extending an Axum service with a companion CLI and needs this lesson's pattern to be clear enough for review, testing, and future maintenance.
- File: src/config.rs
- File: tests/ownership-config-and-results.spec
- File: docs/rust-systems/ownership-config-and-results.md
- Start from the provided Rust snippet and make the intent visible in names and boundaries.
- Apply the checklist item "Borrow when only reading" before adding extra behavior.
- Write down how the implementation changes when borrow fails or becomes slow.
- Taking ownership versus borrowing — Borrowing lets the caller keep ownership. Taking String consumes the caller's value.
- Retained source code:
fn length_owned(value: String) -> usize {
value.len()
}
fn length_borrowed(value: &str) -> usize {
value.len()
}
fn main() {
let name = String::from("Ferris");
println!("{}", length_borrowed(&name));
println!("{}", length_owned(name));
// println!("{name}"); // name was moved
}
- Expected output: 6
6
- Compare the example with the canonical PTLearn implementation.
- Mutating through one mutable borrow — The function borrows the vector mutably long enough to update each String.
- Retained source code:
fn normalize(items: &mut Vec<String>) {
for item in items {
*item = item.trim().to_lowercase();
}
}
fn main() {
let mut tags = vec![String::from(" Rust "), String::from("API")];
normalize(&mut tags);
println!("{tags:?}");
}
- Expected output: ["rust", "api"]
- Compare the example with the canonical PTLearn implementation.
- Returning one of two borrowed values — The returned reference is valid only as long as both inputs are valid for the shared lifetime 'a.
- Retained source code:
fn longest<'a>(left: &'a str, right: &'a str) -> &'a str {
if left.len() >= right.len() {
left
} else {
right
}
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Prefer slices in APIs — A slice accepts arrays, vectors, and subranges without taking ownership.
- Retained source code:
fn average(values: &[f64]) -> Option<f64> {
if values.is_empty() {
return None;
}
Some(values.iter().sum::<f64>() / values.len() as f64)
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
Practice
- Parse one environment setting into a typed value.
- Return Result instead of panicking.
- Add tests for invalid config.
- Choose parameter ownership: For an API that only reads a username and does not store it, choose the best parameter type from String, &String, and &str. Explain why. The guided runner checks deterministic keywords.
- Starter code: parameter:
reason:
- Hint: The function does not need ownership.
- Hint: A string slice is more flexible than &String.
- Reference solution: parameter: &str
reason: borrow the text without taking ownership, and accept String or string literals.
- Accepted answers: &str | borrow
Checklist
- Borrow when only reading
- Own newly created data
- Avoid panic for user input
- Test malformed values
Quiz prompts
- Why accept &str in a read-only normalization function? — Borrowing is the natural choice when a function only needs temporary read access.
- A teammate wants to hide ownership inside a convenient helper. What should you check first? — Place ownership at the boundary that keeps job orchestration service behavior explicit, testable, and reviewable.
- Which artifact best proves this Rust Systems lesson is ready for review? — Production-ready learning needs evidence: a test, trace, command, screenshot, or log that catches the risk again.
- Basic ownership contracts: a teammate says the happy path works, but "Borrow versus own" is still implicit. What should you ask for before merging? — Borrow versus own belongs in the basic stage only when the decision is visible, testable, and tied to a realistic failure mode.
- A reviewer has five minutes to evaluate this basic Rust Systems slice. Which evidence is strongest? — Build the basic Rust config layer: borrowed input helpers, typed config parsing, recoverable Result errors, and tests for malformed values.
- What do lifetime annotations do? — Annotations tell the compiler how references relate; they do not change how long data actually lives.
Flashcards
- Basic ownership contracts: what decision does "Borrow versus own" force you to make? Borrow inputs when reading and return owned values when normalizing or storing data. Evidence prompt: Refactor one helper to accept `&str` and return an owned normalized value.
- Basic ownership contracts: what decision does "Typed config" force you to make? Parse environment and file input once into validated startup types. Evidence prompt: Parse one config value into a typed struct and reject invalid input with Result.
- Basic ownership contracts: what decision does "Recoverable Result" force you to make? Use Result for user, config, and environment failures instead of panics. Evidence prompt: Replace one panic path with a Result and a test for the error case.
- In Rust Systems, what should you remember about ownership? ownership matters here because it supports "Borrow read-only inputs.".
- In Rust Systems, what should you remember about borrow? borrow matters here because it supports "Return owned normalized values.".
- In Rust Systems, what should you remember about Result? Result matters here because it supports "Use Result for recoverable configuration errors.".
- In Rust Systems, what should you remember about enum? enum matters here because it supports "Borrow read-only inputs.".
Labs
- Ship a ownership, config, and results slice — Extend an Axum service with a companion CLI with a small but reviewable feature that proves the lesson's architecture in code.
- Build the basic Rust config layer: borrowed input helpers, typed config parsing, recoverable Result errors, and tests for malformed values.
- Refactor one helper to accept `&str` and return an owned normalized value.
- Parse one config value into a typed struct and reject invalid input with Result.
- Replace one panic path with a Result and a test for the error case.
- Parse one environment setting into a typed value.
- Return Result instead of panicking.
- The lab demonstrates the basic ownership contracts outcome without skipping earlier contract evidence.
- Each subtopic has a concrete artifact: Borrow versus own, Typed config, Recoverable Result.
- The review notes explain how this level changes ownership, verification, or operational risk.
Challenge
- Review-ready ownership, config, and results (Stretch) — Build the basic Rust config layer: borrowed input helpers, typed config parsing, recoverable Result errors, and tests for malformed values.
- All checklist items are either implemented or documented with a reason.
- The change can be understood from src/config.rs plus one short note.
- The concept diagram names ownership, failure handling, and verification points.
- A teammate could run the verification steps without asking for hidden context.
Canonical lesson URL