08 — Rust Interview Prep
Priority: MEDIUM — Rust is your differentiator. Most backend candidates don’t know Rust. Even basic Rust knowledge sets you apart. Focus on concepts, not memorizing syntax.
Table of Contents
- Why Rust Matters for Your Profile
- Ownership, Borrowing & Lifetimes
- Error Handling
- Concurrency & Async Rust
- Traits & Generics
- Smart Pointers & Memory
- Rust vs Python / Rust vs Go
- Axum & Web Development in Rust
- Common Interview Questions
- Resources
Why Rust Matters for Your Profile
How to position Rust in interviews:
1. "I use Rust for performance-critical backend services and tooling,
alongside Python for rapid development and data processing."
2. "My open-source project RediServe is an HTTP API for Redis built
with Axum and Tokio, demonstrating async connection pooling and
high-performance request handling."
3. "Rust's type system and ownership model align with how I think
about backend reliability — preventing bugs at compile time rather
than discovering them in production."
4. "I'm not a Rust-only engineer — I choose the right tool for the job.
Python for APIs and data pipelines, Rust for performance-sensitive
tooling and services."
When Rust comes up:
- Don't oversell — be honest about your experience level
- Focus on concepts (ownership, lifetimes, fearless concurrency)
- Relate to your RediServe project and backend experiments
- Show curiosity and depth of understanding
Ownership, Borrowing & Lifetimes
Ownership Rules
// Rule 1: Each value has exactly ONE owner
let s1 = String::from("hello");
// Rule 2: When owner goes out of scope, value is dropped
{
let s2 = String::from("hello");
} // s2 dropped here, memory freed
// Rule 3: Ownership can be MOVED (not copied for heap data)
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED to s2. s1 is no longer valid.
// println!("{}", s1); // ERROR: value moved
// Copy types (stack-only): integers, floats, bool, char, tuples of Copy types
let x = 5;
let y = x; // x is COPIED. Both x and y are valid.
println!("{} {}", x, y); // Works fine
Borrowing
// Immutable borrow (&T) — read-only, multiple allowed
fn print_length(s: &str) {
println!("Length: {}", s.len());
}
let s = String::from("hello");
print_length(&s); // borrow s
print_length(&s); // can borrow again — s still valid
// Mutable borrow (&mut T) — read+write, only ONE at a time
fn push_char(s: &mut String) {
s.push('!');
}
let mut s = String::from("hello");
push_char(&mut s);
// RULES:
// ✓ Multiple immutable borrows simultaneously
// ✓ Exactly one mutable borrow at a time
// ✗ Cannot have mutable + immutable borrows simultaneously
//
// WHY: Prevents data races at COMPILE TIME
Lifetimes
// Lifetimes ensure references don't outlive their data
// The compiler usually infers lifetimes (lifetime elision)
fn first_word(s: &str) -> &str {
// Compiler infers: input and output have same lifetime
&s[..s.find(' ').unwrap_or(s.len())]
}
// When you need explicit lifetimes:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
// 'a means: the returned reference lives AT LEAST as long
// as the shorter of x and y
if x.len() > y.len() { x } else { y }
}
// Struct with references needs lifetime annotation
struct Config<'a> {
name: &'a str,
// Config can't outlive the string it references
}
// 'static lifetime: lives for entire program duration
let s: &'static str = "hello"; // string literals are 'static
Interview Explanation
"Rust's ownership model prevents memory bugs at compile time:
- Each value has one owner — no double-free
- Borrowing rules prevent data races — you can have many readers OR one writer
- Lifetimes ensure references are always valid — no dangling pointers
This eliminates entire classes of bugs that exist in C/C++ (and even Python
at a different level), without needing a garbage collector."
Error Handling
// Rust uses Result<T, E> and Option<T> instead of exceptions
// Option: value that might not exist
fn find_user(id: u32) -> Option<User> {
// Returns Some(user) or None
}
// Result: operation that might fail
fn read_config(path: &str) -> Result<Config, std::io::Error> {
// Returns Ok(config) or Err(error)
}
// The ? operator: propagate errors concisely
fn process() -> Result<String, Box<dyn std::error::Error>> {
let config = read_config("app.toml")?; // returns Err if failed
let data = fetch_data(&config.url)?; // returns Err if failed
Ok(data.to_string())
}
// Pattern matching for explicit handling
match find_user(42) {
Some(user) => println!("Found: {}", user.name),
None => println!("User not found"),
}
// Combinators
let name = find_user(42)
.map(|u| u.name.clone())
.unwrap_or_else(|| "Unknown".to_string());
// Custom error types (using thiserror crate)
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[error("Not found: {0}")]
NotFound(String),
#[error("Validation error: {0}")]
Validation(String),
}
Concurrency & Async Rust
Fearless Concurrency
// Rust's ownership system prevents data races at compile time
// Thread spawning
use std::thread;
let handle = thread::spawn(|| {
println!("Hello from thread");
});
handle.join().unwrap();
// Shared state with Mutex
use std::sync::{Arc, Mutex};
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
// Message passing with channels
use std::sync::mpsc;
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
tx.send("hello").unwrap();
});
let msg = rx.recv().unwrap();
Async/Await with Tokio
// Tokio: async runtime for Rust
use tokio;
#[tokio::main]
async fn main() {
// Spawn concurrent tasks
let task1 = tokio::spawn(async { fetch_data("url1").await });
let task2 = tokio::spawn(async { fetch_data("url2").await });
let (result1, result2) = tokio::join!(task1, task2);
}
// Async function
async fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
let response = reqwest::get(url).await?;
let body = response.text().await?;
Ok(body)
}
// Key concepts:
// - Future: Rust's version of a promise (lazy — doesn't run until polled)
// - Runtime: Tokio provides the executor that polls futures
// - Send + 'static: requirements for spawning tasks across threads
// - select!: wait for first of multiple futures
// - Semaphore: limit concurrency
Comparison: Rust async vs Python asyncio
Similarities:
- Both use async/await syntax
- Both are single-threaded per executor (cooperative)
- Both excel at I/O-bound concurrency
Differences:
- Rust futures are lazy (don't execute until polled)
- Python coroutines start on creation (mostly)
- Rust can spawn across multiple threads (multi-threaded runtime)
- Python has GIL (truly single-threaded)
- Rust async is zero-cost abstraction (compiles to state machines)
- Python async has runtime overhead
Traits & Generics
// Trait: like an interface (defines behavior)
trait Summary {
fn summarize(&self) -> String;
// Default implementation
fn preview(&self) -> String {
format!("{}...", &self.summarize()[..50])
}
}
// Implement trait for a type
struct Article {
title: String,
content: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}: {}", self.title, &self.content[..100])
}
}
// Generics with trait bounds
fn print_summary<T: Summary>(item: &T) {
println!("{}", item.summarize());
}
// Multiple trait bounds
fn process<T: Summary + Display + Clone>(item: &T) { ... }
// Where clause (cleaner for complex bounds)
fn process<T>(item: &T) -> String
where
T: Summary + Display + Clone,
{
item.summarize()
}
// Key traits to know:
// Clone: explicit duplication
// Copy: implicit duplication (stack types)
// Debug: {:?} formatting
// Display: {} formatting
// Send: safe to transfer between threads
// Sync: safe to reference from multiple threads
// From/Into: type conversion
// Iterator: iteration protocol
// Drop: cleanup when value goes out of scope
Smart Pointers & Memory
// Box<T>: heap allocation (single owner)
let b = Box::new(5);
// Used for: recursive types, trait objects, large stack data
// Rc<T>: reference counting (single-threaded)
use std::rc::Rc;
let a = Rc::new(5);
let b = Rc::clone(&a); // both a and b own the value
// Dropped when last Rc is dropped
// Arc<T>: atomic reference counting (thread-safe Rc)
use std::sync::Arc;
let a = Arc::new(5);
let b = Arc::clone(&a);
// Safe to share across threads
// RefCell<T>: interior mutability (runtime borrow checking)
use std::cell::RefCell;
let data = RefCell::new(5);
*data.borrow_mut() += 1; // mutable borrow at runtime
// Common patterns:
// Arc<Mutex<T>>: shared mutable state across threads
// Rc<RefCell<T>>: shared mutable state (single-threaded)
Rust vs Python / Rust vs Go
Rust vs Python:
| Aspect | Rust | Python |
|----------------|----------------------------|---------------------------|
| Speed | ~C/C++ level | ~50-100x slower |
| Memory | No GC, manual management | GC + reference counting |
| Safety | Compile-time guarantees | Runtime errors |
| Concurrency | Fearless (compile-time) | GIL limits threads |
| Dev speed | Slower (compiler is strict)| Faster (dynamic, flexible)|
| Ecosystem | Growing | Massive |
| Use when | Performance critical | Rapid development, data |
Rust vs Go:
| Aspect | Rust | Go |
|----------------|---------------------------|---------------------------|
| Memory | No GC | GC (low latency) |
| Safety | Ownership model | GC handles most issues |
| Concurrency | async/await + threads | Goroutines (lightweight) |
| Learning curve | Steep | Gentle |
| Compile speed | Slow | Very fast |
| Use when | Max performance, systems | Services, DevOps tools |
Your position: "I use Python for APIs and data processing where development
speed matters, and Rust for performance-critical services where every
millisecond counts. I see them as complementary, not competing."
Axum & Web Development in Rust
Reference your RediServe project
// Basic Axum server (from your RediServe project)
use axum::{
extract::{Path, State},
http::StatusCode,
routing::{get, post},
Json, Router,
};
use deadpool_redis::{Config, Pool, Runtime};
#[derive(Clone)]
struct AppState {
redis_pool: Pool,
}
async fn get_key(
State(state): State<AppState>,
Path(key): Path<String>,
) -> Result<Json<serde_json::Value>, StatusCode> {
let mut conn = state.redis_pool.get().await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let value: Option<String> = redis::cmd("GET")
.arg(&key)
.query_async(&mut conn)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
match value {
Some(v) => Ok(Json(serde_json::json!({"key": key, "value": v}))),
None => Err(StatusCode::NOT_FOUND),
}
}
#[tokio::main]
async fn main() {
let redis_cfg = Config::from_url("redis://127.0.0.1/");
let pool = redis_cfg.create_pool(Some(Runtime::Tokio1)).unwrap();
let state = AppState { redis_pool: pool };
let app = Router::new()
.route("/keys/{key}", get(get_key))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
// Key Axum concepts:
// - Extractors: Path, Query, Json, State (dependency injection via types)
// - State: shared application state (connection pools, config)
// - Middleware: tower middleware ecosystem (logging, auth, CORS)
// - Error handling: Result types with IntoResponse
Common Interview Questions
Q: Explain Rust's ownership model.
A: "Every value in Rust has exactly one owner. When the owner goes out of
scope, the value is dropped (freed). Ownership can be transferred (moved)
or temporarily lent (borrowed). This eliminates memory bugs — no double-free,
no use-after-free, no dangling pointers — all verified at compile time."
Q: What prevents data races in Rust?
A: "The borrow checker enforces that you can have either multiple immutable
references OR one mutable reference, but never both. Combined with the
Send/Sync traits for thread safety, this prevents data races at compile
time. It's called 'fearless concurrency' — if it compiles, it's race-free."
Q: What is a dangling reference and how does Rust prevent it?
A: "A dangling reference points to memory that has been freed. Rust's
lifetime system prevents this — the compiler ensures references never
outlive the data they point to. If you try to return a reference to
local data, the compiler rejects it."
Q: How does async/await work in Rust?
A: "Rust's async functions return Futures — lazy values that represent
a computation. They're compiled into state machines by the compiler
(zero-cost abstraction). A runtime like Tokio polls these futures,
executing them when I/O is ready. Unlike Python, Rust's async can
leverage multiple threads via the Tokio multi-threaded runtime."
Q: When would you choose Rust over Python?
A: "When performance is critical: low-latency APIs, data processing
where Python's GIL is a bottleneck, systems programming, or when
I need compile-time safety guarantees. For most web APIs and data
pipelines, Python's development speed wins. They complement each
other — I can write Python extensions in Rust via PyO3."
Q: What is the borrow checker?
A: "It's the part of the Rust compiler that enforces ownership and
borrowing rules. It tracks references through the code and ensures:
no reference outlives its data (lifetimes), no data races (exclusive
mutable access), and no use after move. It catches bugs at compile
time that other languages find at runtime (or never)."
Resources
- The Rust Programming Language (The Book): https://doc.rust-lang.org/book/
- Rust by Example: https://doc.rust-lang.org/rust-by-example/
- Rustlings: https://github.com/rust-lang/rustlings — interactive exercises
- Tokio Tutorial: https://tokio.rs/tokio/tutorial
- Axum docs: https://docs.rs/axum/latest/axum/
- Zero To Production in Rust: https://www.zero2prod.com/ — web dev in Rust
My Notes
Rust concepts I'm comfortable with:
-
Concepts I need to practice:
-
RediServe features I can demo:
-