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 AsyncReadExt::read_exact is 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: 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 — 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. 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.

✓ Check yourself

Answer from memory before opening each one.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.