Coding Fundamentals
Rust patterns interviewers ask about, the DSA suite you should know cold, and the concurrency primitives that come up in async-Rust coding rounds.
String vs &str vs Cow
| Type | What it is | Use when |
|---|---|---|
String | Owned, growable UTF-8 buffer on the heap. | You need to own + mutate text. |
&str | Borrowed string slice. Pointer + length. | Read-only access. Default for function arguments. |
&'static str | Borrowed slice with program-long lifetime. String literals. | Compile-time string constants. |
Cow<'a, str> | Either Borrowed(&'a str) or Owned(String). | Sometimes you need to mutate (then own), sometimes you don't (then borrow). Lazy. |
Box<str> | Owned, immutable, heap-allocated. Smaller than String (no capacity field). | Many small immutable strings stored long-term. |
use std::borrow::Cow;
// Returns borrowed if no replacement needed, owned if replacement happened.
fn redact_email(s: &str) -> Cow<'_, str> {
if !s.contains('@') {
Cow::Borrowed(s)
} else {
Cow::Owned(s.replace(|c: char| c.is_alphanumeric(), "*"))
}
}
Interview test: someone hands you fn parse(s: String) as an API. Senior answer: "Take &str unless you're storing it." Strings should usually be borrowed at API boundaries.
Box, Rc, Arc
| Type | Ownership | Threading | Mutable inside? |
|---|---|---|---|
Box<T> | Unique owner, heap-allocated | Send if T is Send | Yes, via &mut |
Rc<T> | Reference-counted, multiple owners | !Send (single-thread only) | No without interior mutability (RefCell) |
Arc<T> | Atomic reference-counted, multi-owner | Send + Sync if T is Send + Sync | No without interior mutability (Mutex/RwLock) |
Default in service code: Arc<T> for shared state across tasks. Box<T> for trait objects and recursive types. Rc<T> is rare in service code (single-threaded scenarios only).
"Why does this code compile error?" — the candidate code uses Rc inside a tokio::spawn. Tokio tasks move between threads, so values inside them must be Send. Rc is not. Use Arc.
Async cancellation
Three things to know cold:
- Cancellation = drop. When you drop a future, its in-flight work stops at the next
.awaitpoint. Resources owned by the future are released by destructors. - Cancellation is cooperative. If a task is CPU-spinning without awaits, you can't cancel it. The fix is yielding, or moving the work to
spawn_blocking. - Cancel-safety is a property of a future: can it be cancelled mid-execution without leaving state corrupted?
tokio::select!docs flag which futures are cancel-safe;mpsc::Receiver::recvis, butbroadcast::Receiver::recvis not (you may lose a message).
use tokio::select;
use tokio_util::sync::CancellationToken;
async fn work(token: CancellationToken) {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
loop {
select! {
_ = token.cancelled() => { break; } // cooperative cancel
_ = interval.tick() => { do_step().await; }
}
}
cleanup().await; // runs after cancel
}
Channel patterns interviewers love
Request-reply with oneshot
use tokio::sync::{mpsc, oneshot};
enum Req { Compute { input: u64, reply: oneshot::Sender<u64> } }
let (tx, mut rx) = mpsc::channel::<Req>(64);
tokio::spawn(async move {
while let Some(Req::Compute { input, reply }) = rx.recv().await {
let _ = reply.send(input * 2);
}
});
let (rtx, rrx) = oneshot::channel();
tx.send(Req::Compute { input: 21, reply: rtx }).await.ok();
let answer = rrx.await.unwrap(); // 42
Fan-out with broadcast
Many tasks subscribe; each gets a copy of every message. Late subscribers miss prior messages.
Worker pool with bounded mpsc
One channel feeds N worker tasks. The bounded capacity provides backpressure.
let (tx, rx) = tokio::sync::mpsc::channel::<Job>(1024);
let rx = std::sync::Arc::new(tokio::sync::Mutex::new(rx));
for _ in 0..16 {
let rx = rx.clone();
tokio::spawn(async move {
loop {
let job = { rx.lock().await.recv().await };
match job {
Some(j) => process(j).await,
None => break, // channel closed
}
}
});
}
Note: a single shared Mutex<Receiver> is the simple form. For higher throughput, give each worker its own receiver via cloning, or use a work-stealing crate.
Error type design
For library or service code (where callers branch on errors), define an enum with thiserror:
- One variant per failure mode the caller might handle differently.
- Include
#[from]for source errors that should propagate transparently via?. - Add
retryable: boolon transient variants. Or use a methodfn is_retryable(&self) -> bool. - Don't expose internal panics — convert them at the boundary.
#[derive(thiserror::Error, Debug)]
pub enum ToolError {
#[error("tool not found: {0}")]
NotFound(String),
#[error("schema validation failed: {0}")]
InvalidArgs(String),
#[error("unauthorized: user {user} cannot call {tool}")]
Unauthorized { user: String, tool: String },
#[error("upstream error: {0}")]
Upstream(#[source] BackendError),
#[error("timeout after {0:?}")]
Timeout(std::time::Duration),
}
impl ToolError {
pub fn is_retryable(&self) -> bool {
matches!(self, ToolError::Upstream(_) | ToolError::Timeout(_))
}
}
Trait objects vs generics
When you need polymorphism:
| Static (generics) | Dynamic (trait objects) | |
|---|---|---|
| Syntax | fn f<T: Tool>(t: T) | fn f(t: &dyn Tool) or Box<dyn Tool> |
| Dispatch | Monomorphized at compile time | Vtable lookup at runtime |
| Binary size | One copy per concrete type | One copy |
| Performance | Slightly faster (inlining) | Slightly slower (indirect call) |
| Use when | Hot path; small set of types | Plugin registries; many types; heterogeneous collections |
For a tool registry (heterogeneous, runtime-known set), Box<dyn Tool> is right. For a generic worker that processes one type at a time, generics are right.
DSA — the suite you must know cold
Live coding rounds for Rust infra roles are typically not LeetCode-heavy, but you should be fluent in:
| Pattern | Typical problems |
|---|---|
| Hash maps / sets | Two-sum, group anagrams, longest substring without repeat |
| Sliding window | Max-sum subarray of size k, longest substring with k distinct |
| Two pointers | Sorted-array two-sum, three-sum, container with most water |
| Stacks / monotonic stacks | Valid parens, next-greater-element |
| BFS / DFS on graphs | Connected components, topological sort, shortest path (unweighted) |
| Heaps / priority queues | Top-k, merge k sorted |
| Tries | Autocomplete, prefix matching |
| Binary search | On sorted arrays, on answer space |
| Intervals | Merge intervals, meeting rooms |
| Streams | Reservoir sampling, top-k from stream |
For an AI-infra interview specifically, expect at least one of: rate limiting, LRU cache, retry-with-backoff, request coalescing. Those are in 11.
Concurrency primitives
Beyond channels and mutexes, know:
- Semaphore — bound concurrent operations.
tokio::sync::Semaphore. - Notify — single-producer, single-consumer "something happened" signal.
- Barrier — wait until N tasks reach a point.
- JoinSet — spawn N tasks, await any/all.
- Atomics —
AtomicU64,AtomicBool, etc. UseOrdering::Relaxedfor counters,Acquire/Releasefor cross-thread synchronization. - arc-swap — atomic
Arcswap. Great for "config you can hot-reload." - dashmap — sharded concurrent hashmap.
- parking_lot — faster, smaller
Mutex/RwLockfor sync contexts.
Big-O cheat sheet for Rust collections
| Type | get | insert | remove | iter |
|---|---|---|---|---|
Vec<T> | O(1) indexed | O(1) amortized push, O(n) middle | O(n) middle, O(1) pop | O(n) |
VecDeque<T> | O(1) indexed | O(1) front/back | O(1) front/back | O(n) |
HashMap<K,V> | O(1) avg | O(1) avg | O(1) avg | O(n) |
BTreeMap<K,V> | O(log n) | O(log n) | O(log n) | O(n) sorted |
BinaryHeap<T> | O(1) peek | O(log n) push | O(log n) pop | O(n) |
Defaults: HashMap for general key-value (uses SipHash; switch to ahash/fxhash in perf-sensitive code). BTreeMap when you need sorted iteration or range queries. Vec for everything sequenced.
"I default to Vec<T> + linear scan for N<100 — cache locality wins until then." Knowing where the constants matter is a senior tell.