Section C · Coding

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

TypeWhat it isUse when
StringOwned, growable UTF-8 buffer on the heap.You need to own + mutate text.
&strBorrowed string slice. Pointer + length.Read-only access. Default for function arguments.
&'static strBorrowed 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

TypeOwnershipThreadingMutable inside?
Box<T>Unique owner, heap-allocatedSend if T is SendYes, via &mut
Rc<T>Reference-counted, multiple owners!Send (single-thread only)No without interior mutability (RefCell)
Arc<T>Atomic reference-counted, multi-ownerSend + Sync if T is Send + SyncNo 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).

Common interview trap

"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:

  1. Cancellation = drop. When you drop a future, its in-flight work stops at the next .await point. Resources owned by the future are released by destructors.
  2. 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.
  3. 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::recv is, but broadcast::Receiver::recv is 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: bool on transient variants. Or use a method fn 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)
Syntaxfn f<T: Tool>(t: T)fn f(t: &dyn Tool) or Box<dyn Tool>
DispatchMonomorphized at compile timeVtable lookup at runtime
Binary sizeOne copy per concrete typeOne copy
PerformanceSlightly faster (inlining)Slightly slower (indirect call)
Use whenHot path; small set of typesPlugin 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:

PatternTypical problems
Hash maps / setsTwo-sum, group anagrams, longest substring without repeat
Sliding windowMax-sum subarray of size k, longest substring with k distinct
Two pointersSorted-array two-sum, three-sum, container with most water
Stacks / monotonic stacksValid parens, next-greater-element
BFS / DFS on graphsConnected components, topological sort, shortest path (unweighted)
Heaps / priority queuesTop-k, merge k sorted
TriesAutocomplete, prefix matching
Binary searchOn sorted arrays, on answer space
IntervalsMerge intervals, meeting rooms
StreamsReservoir 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.
  • AtomicsAtomicU64, AtomicBool, etc. Use Ordering::Relaxed for counters, Acquire/Release for cross-thread synchronization.
  • arc-swap — atomic Arc swap. Great for "config you can hot-reload."
  • dashmap — sharded concurrent hashmap.
  • parking_lot — faster, smaller Mutex/RwLock for sync contexts.

Big-O cheat sheet for Rust collections

Typegetinsertremoveiter
Vec<T>O(1) indexedO(1) amortized push, O(n) middleO(n) middle, O(1) popO(n)
VecDeque<T>O(1) indexedO(1) front/backO(1) front/backO(n)
HashMap<K,V>O(1) avgO(1) avgO(1) avgO(n)
BTreeMap<K,V>O(log n)O(log n)O(log n)O(n) sorted
BinaryHeap<T>O(1) peekO(log n) pushO(log n) popO(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.

Senior signal

"I default to Vec<T> + linear scan for N<100 — cache locality wins until then." Knowing where the constants matter is a senior tell.