Skip to main content

Rust engineer coding interview questions

11 questions on coding for rust engineer candidates. Expect prompts such as “Implement a generic stack with ownership semantics” and “Implement Display and From for a custom error type”, each with a worked answer outline and the follow-ups interviewers push on.

Questions

Showing 1 to 11 of 11 coding questions.

As asked

Implement a generic Stack<T> using a Vec as the backing store. It must have push, pop returning Option<T>, and peek returning Option<&T>. Do not use any external crates.

Sample answer outline

Candidate should write a newtype struct wrapping Vec<T>, implement push via Vec::push, pop via Vec::pop, and peek via self.data.last(). Strong answer also implements Display or Iterator, discusses why peek returns a shared reference tied to the stack's lifetime, and notes that this is functionally equivalent to how std's Vec already works.

Reference implementation (rust)

Rust
struct Stack<T> {
    data: Vec<T>,
}

impl<T> Stack<T> {
    fn new() -> Self { Stack { data: Vec::new() } }
    fn push(&mut self, item: T) { todo!() }
    fn pop(&mut self) -> Option<T> { todo!() }
    fn peek(&self) -> Option<&T> { todo!() }
}

Expect these follow-ups

  • How would you implement this so it is Send + Sync?
  • How would you add a size limit and return an error on overflow?
data-structuresgenericsownershipvec

As asked

Define a ParseError enum with variants InvalidNumber(String) and UnexpectedEnd. Implement std::fmt::Display and std::error::Error for it. Also implement From<std::num::ParseIntError> so the ? operator works.

Sample answer outline

Candidate should implement Display using write!(f, ...) matching each variant, implement Error with empty body (or source returning the wrapped error), and implement From<ParseIntError> for ParseError by mapping the error to InvalidNumber. Strong answer notes that thiserror would generate all of this with attribute annotations.

Reference implementation (rust)

Rust
use std::fmt;

#[derive(Debug)]
pub enum ParseError {
    InvalidNumber(String),
    UnexpectedEnd,
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        todo!()
    }
}

impl std::error::Error for ParseError {}

impl From<std::num::ParseIntError> for ParseError {
    fn from(e: std::num::ParseIntError) -> Self { todo!() }
}

Expect these follow-ups

  • How does implementing Error::source enable tools like anyhow to print an error chain?
  • Why is the Error trait's source method preferred over the deprecated cause method?
error-handlingtraitsdisplayfromthiserror

As asked

Write a small ring buffer suitable for bytes received from a UART interrupt handler. Explain the overflow policy.

Sample answer outline

Use a fixed-size buffer with head and tail indices so no allocation happens at runtime. The interrupt handler should do the minimum work: attempt to push the byte and record overflow if the buffer is full. Define the overflow policy explicitly, either drop newest, drop oldest, or set an error flag for the consumer. In real firmware, indices shared with an ISR need atomic or critical-section handling depending on the platform width and concurrency model. Candidates often write a dynamic vector or ignore the full versus empty ambiguity.

Reference implementation (rust)

Rust
struct Ring<const N: usize> {
    buf: [u8; N],
    head: usize,
    tail: usize,
    full: bool,
}

impl<const N: usize> Ring<N> {
    fn new() -> Self {
        Self { buf: [0; N], head: 0, tail: 0, full: false }
    }

    fn push(&mut self, byte: u8) -> bool {
        if self.full {
            return false;
        }
        self.buf[self.head] = byte;
        self.head = (self.head + 1) % N;
        self.full = self.head == self.tail;
        true
    }

    fn pop(&mut self) -> Option<u8> {
        if !self.full && self.head == self.tail {
            return None;
        }
        let byte = self.buf[self.tail];
        self.tail = (self.tail + 1) % N;
        self.full = false;
        Some(byte)
    }
}

Expect these follow-ups

  • How would this change for DMA-backed UART receive?
  • What makes the shared indices safe or unsafe on an 8-bit MCU?
  • When would you drop oldest instead of newest?
uartring-bufferno-alloc

As asked

Implement a singly linked list in safe Rust supporting push_front and pop_front. Why does a naive recursive struct not compile, and how do you fix it?

Sample answer outline

The naive struct List { next: Option<List> } is an infinite-size type. Fix with Box<List> to give it a known pointer size. Candidate should implement Node as enum { Cons(T, Box<Node<T>>), Nil } or use Option<Box<Node<T>>>, then implement push_front by prepending and pop_front by moving the head out. Should discuss why you would prefer VecDeque or std::collections::LinkedList over a hand-rolled implementation.

Reference implementation (rust)

Rust
enum Node<T> {
    Cons(T, Box<Node<T>>),
    Nil,
}

struct List<T> {
    head: Node<T>,
}

impl<T> List<T> {
    fn push_front(&mut self, val: T) { todo!() }
    fn pop_front(&mut self) -> Option<T> { todo!() }
}

Expect these follow-ups

  • Why is a doubly linked list much harder to implement in safe Rust?
  • What does the 'Too Many Linked Lists' book teach about Rust's ownership model?
data-structureslinked-listownershipboxrecursion

As asked

Write an async function that calls an async operation and returns an error if it does not complete within a given Duration. Do not use tokio::time::timeout directly; implement the select yourself to show you understand the mechanism.

Sample answer outline

Candidate writes tokio::select! with two branches: the operation future and tokio::time::sleep(duration). If sleep completes first, return Err(TimeoutError). Should discuss why this is cancellation-safe for the sleep branch (sleep is always safe to cancel) and possibly not for the operation branch depending on what it does. Bonus for noting tokio::time::timeout does the same thing internally.

Reference implementation (rust)

Rust
use std::time::Duration;
use tokio::time::sleep;

async fn with_timeout<F, T>(op: F, dur: Duration) -> Result<T, &'static str>
where
    F: std::future::Future<Output = T>,
{
    tokio::select! {
        // fill in the branches
    }
}

Expect these follow-ups

  • What is tokio::time::timeout_at and when is it more convenient?
  • How do you implement a deadline that spans multiple await points without re-measuring each time?
asynctokiotimeoutselecttime

As asked

You have a Vec<Image> and need to apply a CPU-heavy encode function to each element, collecting results in order. Write the rayon-based parallel version and explain what par_iter gives you over a thread pool you built manually.

Sample answer outline

Candidate writes images.par_iter().map(|img| encode(img)).collect::<Vec<_>>() and explains that rayon uses a work-stealing thread pool, preserves output order through its parallel iterator protocol, and handles thread count automatically based on CPU count. Contrast with manual spawn: rayon eliminates the synchronization boilerplate, handles panics cleanly, and avoids over-subscription.

Reference implementation (rust)

Rust
use rayon::prelude::*;

struct Image { data: Vec<u8> }

fn encode(img: &Image) -> Vec<u8> {
    // CPU-heavy encoding
    img.data.clone()
}

fn encode_all(images: &[Image]) -> Vec<Vec<u8>> {
    // implement with rayon
    todo!()
}

Expect these follow-ups

  • What happens if encode panics inside a rayon parallel iterator?
  • How do you limit rayon to use a specific number of threads?
rayonconcurrencyparallelismiteratorsperformance

As asked

Given the JSON below, write the Rust structs and serde derive annotations needed to deserialize it, including the nested tags array and optional description field that may be absent or null.

Sample answer outline

Candidate writes nested structs with #[derive(Deserialize)], uses Option<String> for the optional field, Vec<Tag> for the array, and applies #[serde(rename_all = "camelCase")] if needed. Should handle absent vs null with Option and discuss #[serde(default)] for absent-means-default cases. Strong answer discusses the difference between missing key and explicit null in JSON.

Reference implementation (rust)

Rust
// Target JSON:
// {
//   "id": 42,
//   "title": "Example",
//   "description": null,
//   "tags": [{"name": "rust", "weight": 1}]
// }

use serde::Deserialize;

#[derive(Deserialize, Debug)]
pub struct Post {
    // add your fields here
}

#[derive(Deserialize, Debug)]
pub struct Tag {
    // add your fields here
}

Expect these follow-ups

  • How do you deserialize a field that could be either a string or an integer?
  • What is #[serde(flatten)] and when does it help?
serdejsondeserializationstructsoption

As asked

Implement an LRU cache with a fixed capacity supporting get(key) and put(key, value), both in O(1) time. Use Rust's standard library collections only.

Sample answer outline

Classic approach: HashMap for O(1) lookup mapping key to a node pointer, plus a doubly linked list for recency order. In safe Rust, a common trick is to use a HashMap<K, usize> plus a VecDeque where indices serve as handles, accepting O(n) eviction, or use indexmap. Candidate should discuss why a fully correct O(1) doubly linked list in safe Rust requires Rc<RefCell<Node>> and the ownership gymnastics involved.

Reference implementation (rust)

Rust
use std::collections::HashMap;

pub struct LruCache<K, V> {
    cap: usize,
    // your internal state here
}

impl<K: std::hash::Hash + Eq + Clone, V: Clone> LruCache<K, V> {
    pub fn new(capacity: usize) -> Self { todo!() }
    pub fn get(&mut self, key: &K) -> Option<&V> { todo!() }
    pub fn put(&mut self, key: K, value: V) { todo!() }
}

Expect these follow-ups

  • Why is a doubly linked list hard to implement efficiently in safe Rust?
  • Would you reach for an external crate like lru here in production, and why?
data-structureslruhashmaplinked-listalgorithms

As asked

Given a Vec<String> of lines, compute the word frequency across all lines in parallel. Use rayon for parallel iteration and DashMap for a concurrent map. Return a HashMap<String, usize>.

Sample answer outline

Candidate uses lines.par_iter(), splits each line into words, and for each word increments the DashMap counter using entry().or_insert(0) pattern. After the parallel phase, converts DashMap into HashMap. Should discuss why a plain HashMap with Mutex is correct but creates contention, and note that rayon's fold + reduce pattern can be more efficient for word counts by accumulating per-thread then merging.

Reference implementation (rust)

Rust
use rayon::prelude::*;
use dashmap::DashMap;
use std::collections::HashMap;

pub fn word_freq(lines: &[String]) -> HashMap<String, usize> {
    let counts: DashMap<String, usize> = DashMap::new();
    // parallel implementation here
    counts.into_iter().collect()
}

Expect these follow-ups

  • How would you do this without DashMap using rayon's fold and reduce?
  • What is the contention pattern on DashMap and when does it become a bottleneck?
rayonconcurrencydashmapalgorithmsperformance

As asked

Implement a bounded multi-producer single-consumer channel with a fixed capacity. Use only std::sync primitives (Mutex, Condvar). The send should block when full and recv should block when empty.

Sample answer outline

Candidate should wrap a VecDeque in a Mutex paired with two Condvars (not_full, not_empty). send acquires lock, waits on not_full while len == cap, pushes, notifies not_empty. recv waits on not_empty while empty, pops, notifies not_full. Should handle clone of the sender handle via Arc and implement a sender counter to signal EOF. Evaluator checks correct use of wait returning the guard.

Reference implementation (rust)

Rust
use std::collections::VecDeque;
use std::sync::{Arc, Condvar, Mutex};

struct Inner<T> {
    buf: VecDeque<T>,
    cap: usize,
}

pub struct Sender<T> { inner: Arc<(Mutex<Inner<T>>, Condvar, Condvar)> }
pub struct Receiver<T> { inner: Arc<(Mutex<Inner<T>>, Condvar, Condvar)> }

// implement send and recv

Expect these follow-ups

  • What is spurious wakeup and how does your Condvar loop handle it?
  • How would you make this lock-free using an atomic ring buffer?
concurrencychannelmutexcondvarsync

As asked

Implement an async function that takes a Vec of futures and returns a Vec of their results, driving all futures concurrently without spawning any tasks. Do not use futures::future::join_all.

Sample answer outline

Candidate should pin each future using Box::pin or pin_mut!, store them in a Vec, and write a poll loop that iterates all pending futures calling poll on each, removing completed ones and recording results. Should use std::future::poll_fn or implement Future manually for the combinator. Strong answer notes that this is exactly what futures::future::join_all does and why it has worse performance than spawning tasks for truly independent work.

Reference implementation (rust)

Rust
use std::future::Future;
use std::pin::Pin;

pub async fn join_all<T>(futures: Vec<Pin<Box<dyn Future<Output = T>>>>) -> Vec<T> {
    todo!()
}

Expect these follow-ups

  • Why does driving multiple futures in a single poll loop not run them in parallel?
  • What does FuturesUnordered offer over a manual poll loop?
asyncfutureconcurrencypincombinators