
Rust Memory Management: The Playroom Analogy
In a previous post, we revisited the fundamentals of the Ada language using a project-based approach. Titled 'Introduction to Ada: a project-based exploration with rosettas', that article walked through the development of a fun, end-to-end program.
Inspired by the classic Spirograph™ toy, the project served as an advanced 'Hello, World.' Instead of printing a simple string, the program generated an SVG file displaying animated rosetta curves.
The goal? To show that Ada, a language famous for its role in safety-critical systems and its strong focus on reliability, is also a language that is easy and fun to learn.

Rust: The Learning Curve Is Steep but Isn't a Cliff
Following this approach to simplicity and safety, let’s turn to Rust, a modern language making waves in the high-integrity world. The goal for this article is to show (like we did previously, with Ada) that its perceived complexity is often overstated. For those hungry to see that direct comparison immediately, the Rust port of the Ada Rosetta project code is already available on GitHub.
However, rather than diving straight into that Spirograph port, we will take a different approach here. To truly grasp the ownership model, we won't look at the rosettas this time; instead, we will focus on a project that utilizes something like the Etch A Sketch™ toy, not as a complex application, but purely as a pedagogical analogy to help visualize concepts that can be notoriously hard to grasp.
Let's be clear: Rust is complex. It introduces a high number of new concepts and an entirely new approach to memory. Even with the abundance of excellent resources available, getting comfortable can be challenging. This is especially true when dealing with the infamous Borrow Checker: that strict compiler feature which quickly becomes the best coding friend you never knew you needed, but which can feel like constantly wrestling an invisible octopus.
To demystify this critical area, we need a new perspective, and this article takes on the challenge of explaining Rust memory management with that simple, relatable playroom analogy.
Children and Toys in the Playroom: A Rust Ownership Analogy
Let's imagine a playroom to understand Rust's memory system.
- Children are like variables (bindings)
- Toys are the non-copy values in memory (e.g., complex data structures)
- Our goal is to prevent a mess (memory corruption, leaks, and other runtime tantrums)
Historically, software developers have managed memory in two primary ways:
- Manual allocation/deallocation: Commonly seen with C/C++, this approach demands total and perfect organization and planning from the start. The usage of every toy must be meticulously documented: when the children take it, where it is used, and precisely when they must give it back. If this documentation is not exhaustive then the entire playroom quickly becomes a mess. Toys are forgotten and never given back (memory leaks), children start fighting over who has control of a toy (data races/double frees), or one child desperately tries to play with a toy that was already given back (use-after-free/memory corruption). In a real-world scenario, this discipline fails easily, leading to memory corruption, leaks, and a huge mess for the developers to troubleshoot later (bugs and security flaws).
- Garbage collection (GC): An adult (the GC) steps into the playroom periodically to clean up all the abandoned toys and messes. This works well most of the time, but it has a catch: the cleaning process can be unpredictable and stops the children from playing (pauses runtime execution).
Rust introduces a third approach, enforced by its clever compiler feature, the Borrow Checker. In our playroom analogy, the Borrow Checker is like a nanny with superpowers who stays in the room before playtime begins. This nanny doesn't clean up periodically (like the GC) but instead checks every single interaction the children plan to have with the toys before they are allowed to play.
This check is based on three very simple rules:
- Each toy has one owner (a single variable owns a value).
- Only the owner can play with the toy (the owner can use the value, or pass permission to others).
- Borrowing is strict: You can either lend the toy to one child for exclusive play (mutable reference), OR you can lend it to many children just to look at and share (immutable references). You can never do both simultaneously.
If the children break these rules, the Borrow Checker nanny simply doesn’t allow the game to start (compile-time error). The result? The mess is prevented entirely, and once the game is running, the children behave perfectly.
Ownership: The Single Owner Analogy
In Rust, every non-copy value has a single owner. This is the foundation of its memory model. For example, imagine a group of children in a playroom around a single, unique toy, like an Etch A Sketch™, that can be modified.
- The Rule: Only one child can hold the toy at a time.
- What it means in Rust: A variable is the sole owner of its data. This eliminates the risk of two different parts of your program trying to modify the same data at the same time, which is the root cause of many bugs.
When the child (variable) leaves the room (goes out of scope), the toy is immediately and automatically put away (the memory is freed). There's no need for a manual cleanup function or a garbage collector. This automatic freeing process is called Dropping.
If the owning child wants to let another child play with the toy, they must physically hand it over.
- The Transfer: When a variable is assigned to another or passed to a function, ownership is moved. The first child no longer has the toy, and the second child is now solely responsible for it.
Borrowing: Sharing Without Giving Up
Rust also provides a mechanism for temporary sharing, called Borrowing. The owner of the toy might say, "You can look at what I'm drawing, but you can’t modify it."
- Immutable Borrow (Shared/Read-Only Reference):
- The Rule: Any number of children can just watch the drawing being made.
- What it means in Rust: You can have many immutable references (shared borrows) to a piece of data. They can read the data but cannot modify it.
- Mutable Borrow (Exclusive/Write Reference):
- The Rule: Only one other child can hold the toy temporarily to make a change, but no one else can even look while they're doing it.
- What it means in Rust: You can have one mutable reference (exclusive borrow) to the data. While this mutable borrow is active, no other references (mutable or immutable) are allowed. This is how Rust prevents data races at compile time.
The Borrow Checker: The Adult in the Room
Who enforces these rules? The Borrow Checker!
- In the Playroom: The adult is quietly present, ensuring the children follow the three rules of ownership and borrowing. If a child tries to break a rule (e.g., two children try to draw at once), the adult stops them before a mess is made.
- In Rust: The Borrow Checker is a part of the compiler that enforces all the ownership and borrowing rules. If your code breaks a rule, it simply will not compile.
This is the key takeaway: unlike the unpredictable overhead of a GC, or the manual error-prone methods of the past, the Borrow Checker does its work entirely at compile time. This provides the best of all worlds: predictable performance and guaranteed memory safety.
Rust Memory Management in Practice: The MagicScreen Demo
The following Rust code provides a practical, executable demonstration of all the core memory management concepts discussed in the playroom analogy: Ownership, Move semantics, the automatic cleanup known as Dropping, and the two forms of temporary sharing: Mutable Borrowing (&mut) and Immutable Borrowing (&). By tracking the MagicScreen structure, this simple project allows you to trace exactly how the Borrow Checker's rules govern data access and lifetime.
// The MagicScreen: A data structure that represents a toy.
struct MagicScreen {
current_drawing: String,
}
// The Drop Trait (Automatic Cleanup)
// When the owner's variable goes out of scope, Rust calls 'drop'.
impl Drop for MagicScreen {
fn drop(&mut self) {
println!("*** DROP: The toy is put back in the Toy Box.");
}
}
fn main() {
// OWNERSHIP: Elena gets the toy.
// 'toy_for_elena' now OWNS the MagicScreen data.
let toy_for_elena = MagicScreen {
current_drawing: String::from("A funny monkey!"),
};
println!("\n1. Owner Elena has the toy (Drawing: {}).",
toy_for_elena.current_drawing);
// MOVE (Transferring Ownership): Elena gives the toy to Olivia.
// The data is MOVED. 'toy_for_elena' is now INVALID.
// Data can only have one OWNER.
// Ownership moved from Elena to Olivia
let mut toy_for_olivia = toy_for_elena;
println!("2. Olivia now has the toy (Drawing: {}).",
toy_for_olivia.current_drawing);
// --- NEW STEP: OLIVIA MODIFIES HER OWN TOY ---
// OWNER MODIFIES: Olivia (the owner) make a new drawing.
// She erases the monkey and draws a cat.
toy_for_olivia.current_drawing = String::from("A silly cat!");
println!("3. Owner Olivia creates a new one (Drawing: {}).",
toy_for_olivia.current_drawing);
// MUTABLE BORROW: Sofia BORROWS the toy to change the drawing.
// Olivia lends the toy out using '&mut'. This is exclusive.
{
// Mutable borrow (&mut)
let toy_for_sofia_mut_borrow = &mut toy_for_olivia;
// --- MODIFICATION: Sofia ADDS a Hat to the Silly Cat ---
// Sofia modifies the existing state
toy_for_sofia_mut_borrow.current_drawing.push_str(" (with a big hat)");
println!("4a. Mutable borrow (Sofia) used to ADD a Hat to the cat.");
} // Mutable borrow ends here (Sofia gives the toy back to Olivia).
// The drawing is now the modified version
println!("4b. Back with Owner Olivia (Drawing: {}).",
toy_for_olivia.current_drawing);
// IMMUTABLE BORROW: Jack and James BORROW (look at the drawing).
// Olivia lends the toy out using '&'. Shared access is granted.
// Multiple immutable borrows can exist simultaneously.
let toy_for_jack_borrow = &toy_for_olivia; // Immutable borrow (&)
let toy_for_james_borrow = &toy_for_olivia; // Another immutable borrow is okay!
println!("5. Immutable borrow (Jack) sees the drawing: {}.",
toy_for_jack_borrow.current_drawing);
println!(" Immutable borrow (James) also sees the drawing: {}.",
toy_for_james_borrow.current_drawing);
}
// Scope ends. 'toy_for_olivia' goes out of scope, 'drop' is called.Conclusion: Strictness for Less Mess!
At this point, your understanding of the memory management system in Rust should be clearer and less daunting. The entire system is built upon two core concepts enforced entirely at compile time by the Borrow Checker.
- Ownership: Every non-copy value in Rust has a single variable that acts as its sole owner.
- This eliminates common bugs caused by multiple parts of a program trying to modify the same data concurrently.
- When the owner variable goes out of scope, the associated memory is immediately and automatically returned to the system (Dropping). This prevents memory leaks.
- Transferring the value to another variable or function moves ownership, invalidating the original variable.
- Borrowing: This mechanism allows temporary access to data without transferring ownership. Access is strictly controlled to maintain safety.
- Immutable Borrow (&): Any number of immutable references can exist simultaneously, allowing read-only access to the data.
- Mutable Borrow (&mut): Only one mutable (exclusive) reference is allowed at any time. While a mutable borrow is active, no other references (mutable or immutable) are permitted.
For those curious to see a bigger picture, the Rust port of the Ada Rosetta project code is waiting for you on GitHub. As stated in our introduction, this serves as an advanced 'Hello, World.' It is an excellent follow-up: a project that sits in the sweet spot between simple and complex, rewarding your efforts with a satisfying, tangible visual result.
If you are keen to go further and understand how Rust’s memory management model can be applied effectively within real-world, high-integrity codebases, the AdaCore team would be pleased to help.
Contact us here to discuss your use of Rust and how we can support your development goals.
Author
Romain Gora

Romain brings over two decades of software development experience from the video game industry and startup leadership to the Field Engineering team at AdaCore in Paris. In this role, he empowers customers to unlock the full potential of their technology stack. With a background in diverse programming languages, Romain focuses on leveraging Ada and Rust to help build software that matters.





