Introduction
Most production applications need to store, organize, and retrieve data efficiently. Whether you're building an e-commerce backend, processing events from Kafka, or managing user sessions, you'll reach for collections repeatedly—and in Rust, choosing the right one shapes both your code's performance and your team's debugging experience. Collections in Rust: a practical guide for developers covers the three workhorses you'll use most: Vec for ordered lists, HashMap for key lookups, and the patterns that make upserting and sorted access straightforward.
Vec for ordered lists
A Vec is Rust's growable array. Unlike a fixed-size array [T; N], a Vec<T> allocates on the heap and resizes as you push elements. This is your go-to for any sequence where order matters and you don't know the final size upfront.
In a ride-hailing backend, you might collect waypoints for a route:
let mut waypoints: Vec<(f64, f64)> = Vec::new();
waypoints.push((40.7128, -74.0060)); // New York
waypoints.push((40.7489, -73.9680)); // Midtown
waypoints.push((40.6892, -74.0445)); // Brooklyn
for (lat, lon) in &waypoints {
println!("Visit: {}, {}", lat, lon);
}
// Access by index (panics if out of bounds)
let first = &waypoints[0];
// Safe access returns Option
if let Some(point) = waypoints.get(10) {
println!("Point at index 10: {:?}", point);
}Key operations on Vec: push() appends in O(1) amortized time. pop() removes the last element. insert(index, value) is O(n) because elements shift. remove(index) also shifts, so avoid it in tight loops on large vectors.
When you need to retain only elements matching a predicate—say, filtering active orders from a batch—use retain():
let mut orders = vec![
(1, "pending"),
(2, "shipped"),
(3, "pending"),
];
orders.retain(|(_, status)| *status == "pending");
// orders now: [(1, "pending"), (3, "pending")]retain() modifies in place and is efficient because it doesn't allocate. For performance-critical code, prefer retain() over creating a new vector with filter() and collect().
HashMap for key lookups
A HashMap<K, V> stores key-value pairs with O(1) average lookup. Use it whenever you need to find a value by a unique identifier—user IDs to profiles, session tokens to session data, SKU codes to inventory counts.
In a SaaS billing system, you'd track subscription states:
use std::collections::HashMap;
let mut subscriptions = HashMap::new();
subscriptions.insert("user_123", ("stripe_sub_1", "active"));
subscriptions.insert("user_456", ("stripe_sub_2", "paused"));
// Check membership
if subscriptions.contains_key("user_123") {
println!("Subscription found");
}
// Retrieve with Option
match subscriptions.get("user_123") {
Some((stripe_id, status)) => println!("Status: {}", status),
None => println!("No subscription"),
}One common pattern: initialize a HashMap from an iterator of items, grouping by some key. In a travel booking system, you might group bookings by destination:
let bookings = vec![
("NYC", 150.0),
("NYC", 200.0),
("LAX", 120.0),
];
let mut by_destination: HashMap<&str, Vec<f64>> = HashMap::new();
for (dest, price) in bookings {
by_destination
.entry(dest)
.or_insert_with(Vec::new)
.push(price);
}Iteration over a HashMap doesn't guarantee order. If you iterate twice over the same map, the order might differ (though in practice, with a small map, it's stable). Never rely on insertion order in a HashMap—use BTreeMap if you need sorted iteration.
Entry API for upserts
The entry API solves a common problem: update a value if the key exists, otherwise insert a default. Without the entry API, you'd do a lookup, then conditionally insert or update—two lookups. The entry API does it in one.
Imagine tracking event counters for analytics. Every time an event occurs, you increment its count:
use std::collections::HashMap;
let mut events = HashMap::new();
// Without entry API: redundant lookups
if events.contains_key("page_view") {
events.insert("page_view", events["page_view"] + 1);
} else {
events.insert("page_view", 1);
}
// With entry API: single lookup
*events.entry("page_view").or_insert(0) += 1;
*events.entry("login_click").or_insert(0) += 1;The entry() method returns an Entry enum. Call or_insert(default) to get a mutable reference to the value, inserting the default if absent. Now you mutate through that reference.
In a CRM, you might accumulate customer interaction logs:
use std::collections::HashMap;
let mut customer_logs: HashMap<u32, Vec<String>> = HashMap::new();
let customer_id = 42;
let interaction = "Email opened".to_string();
customer_logs
.entry(customer_id)
.or_insert_with(Vec::new)
.push(interaction);or_insert_with() accepts a closure, deferring allocation until needed. Use it when the default is expensive to create.
For more control, match on the Entry itself:
use std::collections::HashMap;
let mut cache = HashMap::new();
match cache.entry("key") {
std::collections::hash_map::Entry::Occupied(mut e) => {
e.insert(e.get() + 1); // Modify existing
}
std::collections::hash_map::Entry::Vacant(e) => {
e.insert(0); // Insert new
}
}This pattern is verbose but useful when your logic branches significantly between the occupied and vacant cases.
BTreeMap for sorted keys
A BTreeMap<K, V> keeps keys sorted. Lookup is O(log n), slower than HashMap's O(1), but you gain sorted iteration. Use BTreeMap when you need range queries, iteration in key order, or the smallest/largest key.
In a trading system, you track open orders by price level:
use std::collections::BTreeMap;
let mut bids = BTreeMap::new();
bids.insert(100.5, 50); // price -> qty
bids.insert(99.8, 100);
bids.insert(101.2, 25);
// Iterate in sorted order
for (price, qty) in &bids {
println!("Bid: {} @ {}", qty, price);
}
// Range queries
let high_bids = bids.range(100.0..)
.collect::<Vec<_>>();
// First (smallest) and last (largest)
if let Some((&best_bid, &qty)) = bids.last() {
println!("Best bid: {} at {}", qty, best_bid);
}Range queries are powerful. range(start..end) returns an iterator over key-value pairs within that range, without scanning the whole map. In a calendar or scheduling app, you'd fetch events in a time window with range().
BTreeMap also provides first(), last(), pop_first(), and pop_last(), making it natural for priority queues or finding min/max values alongside lookups.
Common mistakes
Cloning in a loop: Accessing map.get(key).unwrap().clone() repeatedly inside a loop, especially with large strings or complex types. Cache the reference outside the loop or redesign to avoid repeated lookups.
Panicking with indexing: Using vec[i] without bounds-checking. In production code, prefer vec.get(i) which returns Option. Index panics can crash a server.
Assuming HashMap order: Relying on insertion or iteration order. Rust's HashMap uses a randomized hash function (since 1.36), so order is unpredictable. If you need order, use BTreeMap or maintain a separate Vec.
Inefficient upserts without entry API: Doing if map.contains_key(k) { update } else { insert } is two lookups. Use entry(k).or_insert(default) for a single lookup.
Large allocations on Vec::new(): Vec::new() allocates zero capacity initially. If you know the size, call Vec::with_capacity(n) to allocate upfront and avoid repeated reallocation as you push.
Key takeaways
Vec<T>is the default for ordered sequences. Usepush()andget(), avoidinsert()on large vecs, and preferretain()for in-place filtering.HashMap<K, V>provides O(1) lookups and is ideal for identity-based access patterns; iteration order is not guaranteed.- The entry API (
entry().or_insert()) performs upserts in a single lookup, reducing overhead in tight loops. BTreeMap<K, V>keeps keys sorted and enables range queries; use it when order or min/max operations matter.- Avoid panicking indexing, repeated clones in loops, and assuming
HashMaporder. UseOption-returning methods likeget()and design for runtime safety from the start.






Comments
Loading comments…