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, butAsyncReadExt::read_exactis not (a partial read can be lost).
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. Tokio's mpsc::Receiver is not Clone. For higher throughput, use a channel whose receiver clones (async-channel, flume), or shard the work into one mpsc per worker.
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 — wake one waiting task (or all of them) when something happens; it carries no data.
- 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.
✓ Check yourself
Answer from memory before opening each one.
-
A hot-path function redacts an email address only when one is present, and most inputs need no change. What return type avoids an allocation in the common case, and why?
Show answer
Cow<str>. It returns the borrowed input untouched when nothing needs replacing and allocates an owned String only when it does. A plain String return would allocate on every call. See String vs &str vs Cow.
-
Code that uses Rc inside tokio::spawn fails to compile. Explain the error and the fix.
Show answer
Tokio tasks can move between worker threads, so everything they hold must be Send, and Rc is not, since its count is not atomic. Use Arc, and add a Mutex or RwLock if you need to mutate. See Box, Rc, Arc.
-
A select! loop has two branches: a shutdown token and a call that reads a four-byte length prefix with read_exact. What can go wrong, and how would you restructure it?
Show answer
If shutdown wins while read_exact has read part of the prefix, those bytes are dropped with the future, so the stream is misaligned if you continue. read_exact is not cancel-safe. Read into a buffer you own and keep across iterations, or use a framing codec, so cancelling loses nothing. See Async cancellation.
-
Sixteen workers share one Mutex around an mpsc receiver. What limits this design, and what are your alternatives?
Show answer
Every worker serialises on the lock to take a job, so throughput is capped by that lock under heavy load. Use a multi-consumer channel crate, give each worker its own queue with a dispatcher, or use a work-stealing scheduler; keep the shared mutex when contention is low and simplicity matters. See Channel patterns.
-
Choose between generics and trait objects for a tool registry whose tools are looked up by name at run time, and for a worker that handles one request type on a hot path.
Show answer
A tool registry holds many types chosen at run time, so use Box<dyn Tool> and accept an indirect call. The single-type hot-path worker should be generic so the compiler monomorphizes and inlines it. Pick by whether the set of types is open and runtime-known. See Trait objects vs generics.
-
A lookup table of 40 entries is read on every request. Would you use a HashMap, and what does your answer depend on?
Show answer
A Vec with a linear scan is a sound default below roughly a hundred entries, because contiguous memory gives cache locality that hashing cannot beat at that size. Past that, or if lookups are on a hot, large table, use a HashMap, and consider a faster hasher than SipHash. Measure before you decide. See Big-O cheat sheet.
Takeaway
- Take &str at API boundaries unless you store it, and use Cow to borrow in the common case and own only when you must change the text.
- Shared state across Tokio tasks needs Arc and a Mutex or RwLock, because tasks migrate between threads and so their contents must be Send.
- Cancellation means dropping a future at an await point; it is cooperative, and a future is cancel-safe only if dropping it mid-way loses nothing.
- Use oneshot for request-reply, broadcast for fan-out and a bounded mpsc for work, and know that a shared receiver mutex limits a worker pool.
- Model failures with a thiserror enum, one variant per distinct decision, with an is_retryable method.
- Use generics on hot paths with few types and trait objects for open, heterogeneous registries; and keep a small DSA toolkit and the collection costs ready.
The worked problems apply these primitives under a timer. → Coding problems worked
Further reading
- Understanding Ownership — The Rust Programming Language, Rust project. String slices and borrowing at function boundaries.
- Cow — Rust standard library. Clone-on-write semantics and when to return it.
- Send and Sync — The Rustonomicon. Why Rc is not Send and what the marker traits promise.
- select! — docs.rs. Cancellation safety rules for each branch type.
- Select — Tokio tutorial. How select! polls branches and drops the losers.
- std::collections — Rust standard library. When to use which collection, with the cost of each operation.