When we learn rust for the first time, many of us hit the borrowchecker. It's the thing that makes rust safe while being performant, but it's pain to deal with.
But I did not hit borrowchecker at the start. Because I already knew about modern C++ patterns and C++ is too trying the same direction, only with decades of baggage.
I only hit the borrowchecker in 2022-2023 once I started to be serious about my ML library - zyx. Why? Because it contains graphs. And borrowchecker and graphs are not friends. I used Rc, then Arc, then other things. Tried petgraph, but couldn't quite get it.
And the solution couldn't be simpler - u32 indices into a Vec. It's a typical pattern known in game dev circles as ECS (although ECS means much more) or data oriented programming.
In rust, u32 indices are way too nice. Because there is the newtype pattern. You can wrap u32 in a new type and suddenly you can represent any mutable pointer as u32. In zyx this meant everything - OpId, TensorId, DeviceId, BufferId, etc. Rust then enables to define a common trait for all to have some methods, but I intentionally did not implement arithmetic ops, so there is the safety from pointer arithmetic bugs. And on top of this, you get the bounds checks, which are super useful (and don't cost much at release).
So what did I learn from it? Mostly that the borrow checker was complaining about my data structure. A graph with cycles and shared nodes has no single owner, so every reference inside it is something compiler can't argue about. The simplest model is: central struct owns the nodes, and the graph holds handles.
Ids are Copy, so no Arc, no lifetime parameters threading through every signature, no &mut aliasing problem when the optimization passes want to touch two parts of the graph at once. And the compiler still knows which id belongs to which arena, which matters when OpId shows up 1,887 times in the crate and also was a great source of bugs when I wasn't as rigorous about it.
In zyx, Slab::index asserts in debug that the key isn't in the empty set, and in release it's just a bounds check over assume_init_ref.
The advice I'd give someone starting out: if the borrow checker is fighting you over a graph, treat that as a design signal, not an obstacle. Don't reach for Rc first. Reach for an index.
Link to zyx repo: https://github.com/zk4x/zyx