Opening: What System Design Pains Have You Hit?
Last week a friend asked me: “I want to build a service that can handle thousands of concurrent requests, but just thinking about everything I need to deal with makes my head hurt.”
I asked what exactly he meant. He said: parallel event processing, shared state across threads, component-to-component communication, and serializing state snapshots. I laughed—he basically described “pain” four different ways.
I used to do the same thing: hand-roll serialization, manage locks myself, write a pile of channels and synchronization primitives. When I was done, the codebase got bigger—and so did the bug count.
Later I realized the Rust ecosystem already has a few crates that take care of most of that dirty work: Tokio, Crossbeam, and Rkyv. Let’s talk about these three.
Quick Mental Model: How They Simplify Rust System Design

Each of these libraries tackles one major problem area in system design:
Tokio: Async Without Becoming a Scheduler Engineer
Before Tokio, writing async code often meant managing a thread pool, handling blocking I/O carefully, and constantly worrying that one mistake would take your service down.
With Tokio, you mostly write business logic. The runtime takes care of scheduling, the event loop, and task queues.
Think of it like going to a restaurant: you don’t run into the kitchen to coordinate chefs and burners—you sit down and wait for the food.
Crossbeam: Shared State Without Sweating Every Lock
Sharing data across threads is a time bomb: deadlocks, race conditions… any of them can wake you up at 3 a.m.
Crossbeam gives you lock-free queues, atomic data structures, and epoch-based garbage collection. You spend less time debating “how do I lock this correctly,” because it makes the safe path the easy path.
It’s like having a reliable housekeeper: you put things on the table, and they keep everything orderly—no two people fighting over the same glass.
Rkyv: “Serialization” With Lightning-Speed Reads
Traditional serialization (e.g., serde) often has runtime overhead: you pack objects to send them, then unpack them to use them.
Rkyv is different: store data in a format that can be read directly from memory without deserializing. The common claim is that it can be significantly faster than serde in some workloads.
Imagine shipping: others box everything up, tape it, label it, then the receiver unboxes it. Rkyv is more like a purpose-built safe—arrive, swipe a card, and use it.
Practice: A Quick Concurrency “Counter” Walkthrough
Enough talk—code makes it concrete.
First, here’s how Tokio handles scheduling in an async scenario:
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
// No manual thread pool management; Tokio handles it
let handles: Vec<_> = (0..10).map(|i| {
tokio::spawn(async move {
println!("Task {} running", i);
sleep(Duration::from_millis(100)).await;
i * 2
})
}).collect();
let results: Vec<_> = futures::future::join_all(handles).await;
println!("All tasks done, results: {:?}", results);
}
Next, a lock-free queue with Crossbeam:
use crossbeam::queue::SegQueue;
fn main() {
let queue = SegQueue::new();
// Multi-thread safe writes without locks
queue.push(42);
queue.push(100);
// Reads are also lock-free
while let Some(val) = queue.pop() {
println!("Popped: {}", val);
}
}
Finally, a Rkyv example for saving a state snapshot:
use rkyv::{Archive, Serialize, Deserialize, ser::serializers::AlignedSerializer};
use rkyv::util::AlignedVec;
#[derive(Archive, Serialize, Deserialize, Debug)]
struct SystemState {
node_count: u32,
status: String,
}
fn main() {
let state = SystemState {
node_count: 5,
status: "running".to_string(),
};
// Fast serialization into a buffer
let mut buffer = AlignedVec::with_capacity(256);
let mut serializer = AlignedSerializer::new(&mut buffer);
serializer.serialize_value(&state).unwrap();
let archived_bytes = serializer.into_inner();
// Read archived data directly (no deserialization)
let archived = rkyv::archived_root::<SystemState>(&archived_bytes);
println!(
"Archived state: node_count={}, status={}",
archived.node_count, archived.status
);
}
Three snippets, three scenarios:
- Async scheduling → Tokio
- Lock-free sharing → Crossbeam
- Zero-copy-ish reads for snapshots → Rkyv
Common Pitfalls (And What To Do Instead)
Pitfall 1: tokio::spawn runs, but you never get results
If you never join your handles, tasks might panic silently and you won’t notice. Spawned tasks should be awaited and handled.
// Correct: join_all ensures all tasks finish
let results = futures::future::join_all(handles).await;
Pitfall 2: Crossbeam scoped threads still need to be joined
crossbeam::scope is great, but if you forget to join, Rust can panic on scope exit.
// Correct: ensure all threads finish
crossbeam::scope(|s| {
let h = s.spawn(|_| { /* work */ });
h.join().unwrap();
}).unwrap();
Pitfall 3: Rkyv version compatibility
Archived data might not be compatible across versions. In production, plan for versioning and migrations.
Summary: Remember These Three Building Blocks
These crates are like scaffolding for a skyscraper: you might not notice them, but they hold the whole thing up.
Quick API cheat sheet:
| Library | What it solves | Key APIs |
|---|---|---|
| Tokio | Async runtime | #[tokio::main] / tokio::spawn |
| Crossbeam | Lock-free concurrency | SegQueue / scope |
| Rkyv | Fast archiving/serialization | Archive / AlignedSerializer |
Next steps
Run cargo add tokio crossbeam rkyv, then:
- Replace manual thread management with Tokio where it makes sense.
- Try Rkyv as an alternative to
serdefor snapshot/persistence workloads that benefit from fast reads. - For shared-state hotspots, check whether Crossbeam already has a lock-free building block you can use.
If this post helped, share it with a friend who’s fighting concurrency bugs. Questions and ideas are welcome in the comments.
Next time, let’s talk about “How to write a high-performance web service in Rust”—a hands-on, from-scratch build of an API server that can take real load.
