Rust Developer interview prep
A spaced-repetition deck of 136+ Rust Developer interview questions — organised by topic and difficulty, and resurfaced right before you'd forget. Preview a few cards below, then choose access to study the whole track on an Anki-style SM-2 schedule.
7 days free on monthly or yearly · every feature included.
What's covered
Every topic in this track, grouped the way you'd study it.
Ownership, Borrowing & Lifetimes
12 cardsTraits, Generics & Dispatch
11 cardsSmart Pointers & Interior Mutability
7 cardsErrors, Collections & Ecosystem
10 cardsConcurrency: Send/Sync & Atomics
8 cardsAsync/await & Tokio
10 cardsUnsafe, Memory & FFI
7 cardsLive Coding & Applied Tasks
7 cardsSystem Design
29 cardsBehavioral
35 cardsSample questions
A few cards from the deck — reveal each answer, then choose access to study the full set on a schedule.
How does Rust guarantee memory safety without a garbage collector?
How does Rust guarantee memory safety without a garbage collector?
Short answer: Rust checks ownership and borrows statically, at compile time. The borrow checker proves every reference lives no longer than its data and that there's never simultaneous aliasing plus mutation — so use-after-free, double-free and data races are ruled out before the program ever runs. No GC and no reference counting by default: you pay in compile time and learning curve.
In depth:
- Ownership — every value has exactly one owner; when it leaves scope the memory is freed deterministically (
Drop), with no runtime pauses. - Borrowing — the compiler enforces "either many
&Tor one&mut T", which is what kills data races statically. - Lifetimes — the borrow checker matches the lifetimes of references and data, preventing a dangling reference from ever existing.
- Zero-cost abstractions — the compiler pays for safety; there are no checks in the binary.
| Criterion | Rust | GC languages |
|---|---|---|
| When checked | compile time | runtime |
| Runtime cost | zero | GC pauses / refcount |
| Failure mode | compile error | leaks, latency, rare bugs |
⚠️ Common mistake: answering "Rust counts references, like ARC in Swift" or "there's a GC under the hood". By default there's neither — Rc/Arc are an opt-in tool, not the memory-safety mechanism.
Rust has no class inheritance — how do traits and composition replace classic OOP?
Rust has no class inheritance — how do traits and composition replace classic OOP?
Short answer: Traits give interface-style polymorphism — with default methods and blanket impls. A supertrait is an obligation to "also implement that trait," not inheritance of fields and behavior. Code reuse in Rust is built on composition: you embed a type in a struct and delegate (including via Deref).
In depth:
- Polymorphism — a shared interface via
trait+ default methods; statically (generics) or dynamically (dyn). - Blanket impl —
impl<T: Display> ToString for TgivesToStringto everyDisplaytype at once — reuse without a hierarchy. - Supertrait ≠ inheritance —
trait B: Arequires implementingAtoo, but doesn't inherit its state; it's a constraint, not "is-a." - Composition — component fields + method delegation replace class chains; "has-a" instead of "is-a."
| OOP concept | Rust counterpart |
|---|---|
| interface | trait |
| abstract method | trait method with no body |
| default method | default trait method |
| behavior inheritance | composition + delegation |
| interface inheritance | supertrait (trait B: A) |
⚠️ Common mistake: calling a supertrait "inheritance." It drags along no fields or implementation — it's just a requirement to implement another trait.
When do you reach for Box, Rc, or Arc? How do they differ?
When do you reach for Box, Rc, or Arc? How do they differ?
Short answer: Box<T> is a single owner — a plain heap allocation with zero-cost dereference. Rc<T> is shared ownership via a non-atomic reference count, single-threaded only. Arc<T> is the same shared ownership, but the count is updated atomically, so it can be shared across threads (at the cost of atomic operations).
In depth:
Box<T>— one owner, no counter. Use it when a value must live on the heap: recursive structures, trait objects (Box<dyn Trait>), large values. Dereference is free.Rc<T>— several owners in one thread.clone()increments the strong count,dropdecrements it; data is freed when it hits zero. Hands out only&T— put aRefCellinside for mutation.Arc<T>— likeRc, but the count lives in atomics (fetch_add/fetch_sub). ImplementsSend/Sync, fit for multithreading; wrap inMutex/RwLockto mutate.
| Owners | Threads | Type |
|---|---|---|
| one | — | Box<T> |
| many | one | Rc<T> |
| many | several | Arc<T> |
⚠️ Common mistake: reaching for Arc everywhere "just in case". If there's a single owner, Box or even the stack is enough; Arc's atomic operations aren't free, and in one thread they're pure overhead.
When do you use Option vs Result<T, E>?
When do you use Option
Short answer: Option<T> — when a value may simply be absent and the reason doesn't matter (Some/None). Result<T, E> — when an operation can fail and the caller needs the cause (Ok/Err).
In depth:
- Option — models "the value may not be there":
HashMap::getmissed the key,Vec::firston an empty vector.Nonecarries no reason. - Result — models "the operation failed":
File::opendidn't find the file,str::parsecouldn't parse.Err(e)explains what went wrong. - Conversion — they cross over easily:
option.ok_or(err)turnsNoneintoErr, andresult.ok()drops the error and yields anOption.
| Type | Meaning | Typical API |
|---|---|---|
| Option |
value may be absent | HashMap::get, Vec::first |
| Result<T, E> | operation may fail | File::open, str::parse |
⚠️ Common mistake: returning Result where absence is normal (or Option where the caller needs the cause). "Key not found" is an Option, not an error.
What are Send and Sync, and how does the compiler derive them automatically?
What are Send and Sync, and how does the compiler derive them automatically?
Short answer: Send means ownership of a value can be safely moved to another thread; Sync means &T can be safely shared across threads (by definition T: Sync ⇔ &T: Send). These are auto traits: the compiler implements them structurally — a type is Send/Sync if all its fields are. You write nothing by hand.
In depth:
- Structural derivation — the auto trait propagates through fields: a struct of
Sendfields is itselfSend. Raw pointers (*const T,*mut T) deliberately opt out of both, which strips the traits from types built on them. unsafe impl— if you uphold thread safety manually (e.g. wrapping a raw pointer in correct synchronization), you may writeunsafe impl Send for MyType {}. That's an assertion you're responsible for: the compiler can't check it.- Opting out explicitly — a negative impl (
impl !Send for T) is nightly-only; that's howRcis marked insidestd. On stable you strip the trait with a marker field likePhantomData<*const ()>(!Send + !Sync).
| Type | Send | Sync | Why |
|---|---|---|---|
Rc<T> |
no | no | non-atomic reference count |
Arc<T> |
yes | yes | atomic count (when T: Send + Sync) |
Cell<T> / RefCell<T> |
yes | no | unsynchronized interior mutability |
Mutex<T> |
yes | yes | synchronization inside (when T: Send) |
*mut T |
no | no | a raw pointer offers no guarantees |
⚠️ Common mistake: thinking Send and Sync are about a type's "thread-safety in general". Send is about moving ownership between threads, Sync about shared access via a reference; these are distinct guarantees, and a type can have one without the other.
What is a Future, and why does calling an async fn without .await run nothing?
What is a Future, and why does calling an async fn without .await run nothing?
Short answer: A Future is a lazy state machine implementing the Future trait with a poll method. Calling an async fn doesn't start work — it only constructs the future value. The code inside runs only when an executor polls it, i.e. when you .await it or hand it to spawn.
In depth:
async fnreturns a value — a typeimpl Future<Output = T>. The call is a state-machine constructor, not the start of the computation.- Laziness — unlike a thread, a future does not spin in the background on its own. Until something polls it, it's inert.
- Who drives the future — an
.awaitinside another async function, ortokio::spawn, which hands the task to the runtime. Without that, the work never begins.
async fn work() { println!("running"); }
let fut = work(); // nothing happened — no print
fut.await; // only now the body actually runs
⚠️ Common mistake: expecting an async fn to start "like a thread" the moment you call it. A forgotten .await is work that never ran — and often an unused_must_use warning: "futures do nothing unless you .await or poll them."
Ready to make it stick?
Start your first session in under a minute. Your future self, mid-interview, will thank you.
Questions about this track
How should I prepare for a Rust Developer interview?
Study the concepts you'll be asked to explain, not just the ones you can code. RecallDeck's Rust Developer track gives you 136+ curated interview questions and resurfaces each one with an Anki-style SM-2 schedule right before you'd forget it — so the answers are still there under pressure on interview day.
What topics does the Rust Developer track cover?
The Rust Developer track is organised into the core areas Rust Developer interviews actually test, grouped by topic and by difficulty (Concept, Junior, Middle, Senior). You can preview the full outline and sample questions above before signing in.
Is spaced repetition effective for Rust Developer interview prep?
Yes. Actively recalling an answer and grading yourself honestly builds far more durable memory than re-reading notes. RecallDeck schedules each Rust Developer card to reappear at the moment you're about to forget it, so your daily reviews shrink while your recall holds.
Can I try the Rust Developer track before paying?
Yes. Monthly and yearly access include a seven-day trial of the complete Rust Developer track, the full SM-2 scheduler, statistics, flexible pacing, and cram mode. You can cancel online before the first charge.