Vorbereitung auf Rust Interviews

Rust Backend-Entwickler Interviewfragen

15 ausgewählte Rust Backend-Interviewfragen, gruppiert nach Erfahrungsstufe. Nutzen Sie diese, um Grundlagen, praktische Kompromisse und Überlegungen für die Produktionsumgebung auf Senior-Niveau zu überprüfen.

Rust KI-Interview startenKeine Kreditkarte erforderlich. 1 kostenlose Sitzung verfügbar.
Technisches Interview-Training auf EnglischEin Modus, in dem Nicht-Muttersprachler technische Interviews üben können.

Junior Fragen

1Wie funktioniert Ownership in Rust, und was bedeutet das für Kopieren, Ausleihen und gemeinsamen Backend-Zustand?

Ein Wert hat einen Besitzer. Das Verschieben eines Nicht-Copy-Werts überträgt den Besitz; beim Verlassen des Geltungsbereichs wird der noch besessene Wert abgelegt. Eine Funktion kann einen Wert übernehmen, gemeinsam leihen oder exklusiv veränderlich leihen. clone() erzeugt eine neue Instanz nach dem Typvertrag, kann aber wie bei Arc dieselben inneren Daten teilen. Für langlebigen gemeinsamen Zustand eignen sich Handles wie Arc. Wenn er veränderlich sein muss, braucht er passende Synchronisation oder ein anderes Nebenläufigkeitsmodell.

use std::sync::Arc;

struct AppState {
    service_name: String,
}

async fn handler(state: Arc<AppState>, request_id: String) -> String {
    // Borrow: no ownership transfer of the String inside state.
    let name: &str = &state.service_name;

    // Clone only if an owned independent value is needed.
    let owned_name = name.to_owned();

    format!("{owned_name}:{request_id}")
}

fn configure(state: Arc<AppState>) {
    // Cloning Arc increments the reference count; it does not clone AppState.
    let state_for_route = Arc::clone(&state);
    let _ = state_for_route;
}
Diese Frage mit einem KI-Coach beantworten

2Worin unterscheiden sich &T und &mut T beim Ausleihen, und wie beeinflussen sie Rust-Schnittstellen?

&T ist eine gemeinsame Referenz. Mehrere davon dürfen zugleich verwendet werden; gewöhnliche Mutation ist darüber nicht möglich, außer der Typ unterstützt innere Veränderlichkeit. &mut T gewährt exklusiven Zugriff und erlaubt Änderungen. Verwende lesend &T oder passende Slices wie &str, für exklusive Änderungen &mut T oder Besitz. Gemeinsamer Zustand über parallele Handler wird üblicherweise über geteilte Handles und Synchronisation verwaltet, statt jedem Handler direkt &mut auf denselben Zustand zu geben.

struct User {
    name: String,
    login_count: u64,
}

fn display_name(user: &User) -> &str {
    &user.name
}

fn record_login(user: &mut User) {
    user.login_count += 1;
}

fn main() {
    let mut user = User { name: "Ada".into(), login_count: 0 };

    let name = display_name(&user); // shared borrow for reading
    println!("{name}");

    record_login(&mut user); // exclusive mutable borrow for mutation
}
Diese Frage mit einem KI-Coach beantworten

3Wie funktioniert Box in Rust, und welche Probleme löst seine Indirektion?

Box<T> besitzt den enthaltenen Wert. Bei einem Wert mit Speicherbedarf liegt er in einer Heap-Allokation; der Box-Wert hält den Zeiger und bei dynamischer Größe auch nötige Metadaten. Dadurch haben rekursive Typen eine bekannte Größe und dynamisch große Werte wie Box<dyn Trait> lassen sich besitzen. Das Verschieben der Box überträgt Besitz, ohne den gespeicherten Wert zu kopieren. Beim Ablegen der Box wird ihr Wert abgelegt und eine vorhandene Allokation freigegeben.

enum List {
    Cons(i32, Box<List>),
    Nil,
}

let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
Diese Frage mit einem KI-Coach beantworten

4Wofür stehen Option und Result in Rust, und wie behandelst du fehlende Werte und mögliche Fehler?

Option<T> ist Some(T) oder None und beschreibt erwartete Abwesenheit. Result<T, E> ist Ok(T) oder Err(E) und bewahrt Fehlerinformation. Nutze match, if let oder let ... else, wenn Fälle unterschiedlich behandelt werden, und Kombinatoren wie map, and_then, ok_or oder unwrap_or für Umwandlung und Standardwerte. ? gibt Abwesenheit oder Fehler aus einem passenden Rückgabetyp früh weiter. unwrap und expect können paniken; setze sie bei geprüften Invarianten oder Tests ein, nicht für gewöhnlich behandelbare Backend-Fehler.

fn parse_port(s: Option<&str>) -> u16 {
    s.and_then(|v| v.parse::<u16>().ok())
     .unwrap_or(8080)
}

assert_eq!(parse_port(Some("3000")), 3000);
assert_eq!(parse_port(None), 8080);
Diese Frage mit einem KI-Coach beantworten

5Wie funktioniert der Operator ? in Rust, und wie werden verschiedene Fehlertypen verbunden?

Bei Result ergibt expr? im Erfolgsfall den enthaltenen Ok-Wert. Bei Err verlässt es den aktuellen Kontext früh mit einem kompatiblen Fehler. Bei Option liefert es den Some-Wert oder gibt None früh zurück. Bei Result wandelt Rust den Fehler über From in den Fehler des Rückgabetyps um; eine passende Implementierung lässt sich auch mit thiserror erzeugen. ? wandelt Option und Result nicht automatisch ineinander um.

use std::{fs, io, num::ParseIntError};

#[derive(Debug)]
enum AppError {
    Io(io::Error),
    Parse(ParseIntError),
}

impl From<io::Error> for AppError {
    fn from(e: io::Error) -> Self { AppError::Io(e) }
}
impl From<ParseIntError> for AppError {
    fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}

fn read_number(path: &str) -> Result<i32, AppError> {
    let s = fs::read_to_string(path)?; // io::Error -> AppError
    let n = s.trim().parse::<i32>()?;  // ParseIntError -> AppError
    Ok(n)
}
Diese Frage mit einem KI-Coach beantworten

6Wie funktioniert Musterabgleich für Enums, und warum verbessert Vollständigkeit die Korrektheit?

Ein Enum hat benannte Varianten, die Daten tragen können. match wählt ein passendes Muster und kann die Variantendaten zerlegen. Rust verlangt einen vollständigen Match; ein Auffangmuster zählt als Abdeckung. Ohne Auffangmuster kann eine neue Variante Stellen sichtbar machen, an denen eine fachliche Entscheidung fehlt. Ein pauschales Auffangmuster kann diesen Schutz vor unbemerkten neuen Varianten abschwächen.

enum PaymentStatus {
    Pending,
    Paid { receipt_id: String },
    Failed(String),
}

fn message(status: PaymentStatus) -> String {
    match status {
        PaymentStatus::Pending => "Payment is pending".to_string(),
        PaymentStatus::Paid { receipt_id } => format!("Paid, receipt {receipt_id}"),
        PaymentStatus::Failed(reason) => format!("Payment failed: {reason}"),
    }
}
Diese Frage mit einem KI-Coach beantworten

7Was ist ein Newtype in Rust, und wie erhöht er die Typsicherheit für IDs, Geldbeträge und Geheimnisse?

Ein Newtype ist ein eigener Typ um eine bestehende Darstellung, oft ein Tupel-Struct mit einem Feld, etwa struct UserId(Uuid). So kann der Compiler UserId und OrderId oder Cent und andere Einheiten auseinanderhalten. Ein privates Feld mit kontrolliertem Konstruktor kann Validierung erzwingen. Bei Tokens oder Geheimnissen lege Debug, Display und Serialisierung bewusst fest, damit sie den Inhalt nicht versehentlich ausgeben.

use uuid::Uuid;

#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
struct UserId(Uuid);

#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
struct OrderId(Uuid);

struct SecretToken(String);

impl std::fmt::Debug for SecretToken {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("SecretToken(**redacted**)")
    }
}

fn load_user(id: UserId) {
    // query by user id
}

fn example(user_id: UserId, order_id: OrderId) {
    load_user(user_id);
    // load_user(order_id); // does not compile: expected UserId, found OrderId
}
Diese Frage mit einem KI-Coach beantworten

Middle Fragen

8Wie funktioniert Drop in Rust, in welcher Reihenfolge werden Werte bereinigt, und welche Grenzen gibt es bei asynchroner Freigabe?

Wird ein Wert abgelegt, ruft Rust gegebenenfalls Drop::drop(&mut self) auf und legt danach die Felder ab. Lokale Bindungen werden in ihrem Geltungsbereich grundsätzlich in umgekehrter Deklarationsreihenfolge abgelegt; Struct-Felder in Deklarationsreihenfolge. Bei RAII hängt die Freigabe von Dateien, Sperren oder Permits an einem besitzenden Wert. drop ist synchron und kann nicht awaiten. Nutze für zuverlässiges asynchrones Schließen, Spülen oder Herunterfahren eine ausdrückliche async-Operation. Ein erzwungener Prozessabbruch kann Destruktoren umgehen.

struct Guard(&'static str);

impl Drop for Guard {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

struct Pair {
    first: Guard,
    second: Guard,
}

impl Drop for Pair {
    fn drop(&mut self) {
        println!("drop Pair");
    }
}

fn main() {
    let _a = Guard("a");
    let _pair = Pair {
        first: Guard("first"),
        second: Guard("second"),
    };
    let _b = Guard("b");
}
Diese Frage mit einem KI-Coach beantworten

9Was beschreiben Lebensdauern, und wann braucht eine Rust-Schnittstelle explizite Annotationen?

Eine Lebensdauer beschreibt die Gültigkeit einer Referenz. Der Compiler prüft damit, dass sie nicht länger als die referenzierten Daten verwendet wird. Annotationen geben Beziehungen zwischen Referenzen an, ohne die Daten länger leben zu lassen oder Laufzeitlogik hinzuzufügen. In einfachen Signaturen gelten Elisionsregeln. Schreibe Lebensdauern ausdrücklich, wenn etwa bei mehreren Eingaben die Herkunft des Rückgabeverweises unklar ist, ein Typ Referenzen speichert oder eine generische Leihbeziehung Teil des Vertrags ist.

fn longest<'a>(left: &'a str, right: &'a str) -> &'a str {
    if left.len() >= right.len() { left } else { right }
}

struct UserView<'a> {
    name: &'a str,
}

fn main() {
    let a = String::from("short");
    let b = String::from("longer");
    let result = longest(&a, &b);
    let view = UserView { name: result };
    println!("{}", view.name);
}
Diese Frage mit einem KI-Coach beantworten

10Was bedeutet 'static in Rust, und warum spielt es bei gestarteten Tasks und Backend-Schnittstellen eine Rolle?

Eine Referenz mit 'static kann während der ganzen Programmlaufzeit gültig sein, etwa auf ein Stringliteral. T: 'static verlangt, dass T keine zu kurz geliehenen Bestandteile enthält. Ein besessener String kann die Bedingung erfüllen und trotzdem sofort freigegeben werden. Ein Executor kann eine gestartete Aufgabe nach Ende des aufrufenden Rahmens weiter ausführen. Verschiebe daher passende besessene Werte oder geklonte Arc-Handles mit async move in die Aufgabe. Verwende 'static nur, wenn Speichern oder Ausführen die Lebensdauer des Aufrufers wirklich überschreiten kann.

use std::sync::Arc;

struct AppState {
    name: String,
}

fn spawn_background(state: Arc<AppState>, user_id: String) {
    tokio::spawn(async move {
        // The task owns an Arc handle and a String, so it does not borrow this function's stack.
        println!("{}:{user_id}", state.name);
    });
}
Diese Frage mit einem KI-Coach beantworten

11Wie nutzt Rust Traits für Polymorphie, und wie unterscheiden sich Generics, impl Trait und dyn Trait?

Ein Trait beschreibt gemeinsames Verhalten. Ein generischer Parameter wie T: Trait wird gewöhnlich durch Monomorphisierung statisch aufgelöst. impl Trait als Argument ist weitgehend eine Kurzform für einen anonymen generischen Parameter. Als Rückgabe verbirgt es einen konkreten von der Funktion gewählten Typ. dyn Trait ist ein Trait-Objekt hinter einer Referenz oder Box und ruft Methoden dynamisch über eine Vtable auf. Damit sind unterschiedliche Implementierungen zur Laufzeit möglich, sofern der Trait dyn-kompatibel ist.

trait Render { fn render(&self) -> String; }

struct Html;
struct Json;

impl Render for Html { fn render(&self) -> String { "html".into() } }
impl Render for Json { fn render(&self) -> String { "json".into() } }

fn render_generic<T: Render>(x: &T) -> String { x.render() } // static dispatch
fn render_impl_arg(x: &impl Render) -> String { x.render() } // anonymous generic
fn make_renderer() -> impl Render { Html } // one hidden concrete type
fn render_dyn(x: &dyn Render) -> String { x.render() } // dynamic dispatch

fn main() {
    let items: Vec<Box<dyn Render>> = vec![Box::new(Html), Box::new(Json)];
    for item in items { println!("{}", render_dyn(item.as_ref())); }
}
Diese Frage mit einem KI-Coach beantworten

12Was sind assoziierte Typen in Traits, und wann eignen sie sich besser als generische Typparameter?

Ein assoziierter Typ ist Teil des Trait-Vertrags und wird durch die jeweilige Implementierung festgelegt, etwa Iterator::Item. Er passt, wenn pro Implementierung ein natürlicher Typ dazugehört. Ein generischer Trait-Parameter passt, wenn die Auswahl Teil des verwendeten Vertrags sein soll oder derselbe konkrete Typ den Trait für verschiedene Argumenttypen implementieren muss.

trait Repository {
    type Entity;
    fn get(&self, id: u64) -> Option<Self::Entity>;
}

struct User;
struct UserRepo;

impl Repository for UserRepo {
    type Entity = User;
    fn get(&self, _id: u64) -> Option<Self::Entity> { Some(User) }
}

fn load<R: Repository>(repo: &R) -> Option<R::Entity> {
    repo.get(1)
}
Diese Frage mit einem KI-Coach beantworten

Senior Fragen

13Wie setzt du das Transactional-Outbox-Muster in einem Rust-Dienst um?

Speichere Fachdaten und ausgehende Ereignisse atomar in derselben Datenbanktransaktion, etwa mit `sqlx::Transaction`. Die Outbox enthält beispielsweise Ziel-Topic, Nutzdaten und Status oder Erstellungszeit. Ein asynchroner Worker liest ausstehende Zeilen; alternativ kann Change Data Capture (CDC) die Änderungen streamen, etwa mit Debezium. Beim Polling erlaubt `SELECT ... FOR UPDATE SKIP LOCKED` mehreren Workern, bereits gesperrte Zeilen zu überspringen und unterschiedliche Einträge zu bearbeiten. Markiere oder lösche eine Zeile erst nach Bestätigung durch den Broker. Fällt der Worker zwischen Bestätigung und Statusänderung aus, kann er dasselbe Ereignis erneut senden. Verbraucher müssen es daher idempotent verarbeiten.

use sqlx::{PgPool, Postgres, Transaction};
use uuid::Uuid;

pub async fn create_user_with_outbox(
    mut tx: Transaction<'_, Postgres>,
    username: &str,
    email: &str,
) -> Result<(), sqlx::Error> {
    let user_id = Uuid::new_v4();
    sqlx::query!(
        "INSERT INTO users (id, username, email) VALUES ($1, $2, $3)",
        user_id, username, email
    )
    .execute(&mut *tx)
    .await?;

    let payload = serde_json::json!({"event": "UserCreated", "user_id": user_id, "email": email});
    sqlx::query!(
        "INSERT INTO outbox_events (id, topic, payload, status) VALUES ($1, $2, $3, 'PENDING')",
        Uuid::new_v4(),
        "user_events",
        payload
    )
    .execute(&mut *tx)
    .await?;

    tx.commit().await?;
    Ok(())
}

pub async fn run_outbox_relay(pool: PgPool) {
    loop {
        let mut tx = match pool.begin().await { Ok(t) => t, Err(_) => continue };
        let rows = sqlx::query!(
            "SELECT id, topic, payload FROM outbox_events WHERE status = 'PENDING' LIMIT 50 FOR UPDATE SKIP LOCKED"
        )
        .fetch_all(&mut *tx)
        .await;

        if let Ok(events) = rows {
            for event in events {
                // publish_to_broker(&event.topic, &event.payload).await;
                let _ = sqlx::query!("UPDATE outbox_events SET status = 'PROCESSED' WHERE id = $1", event.id)
                    .execute(&mut *tx)
                    .await;
            }
            let _ = tx.commit().await;
        }
        tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
    }
}
Diese Frage mit einem KI-Coach beantworten

14Wie entwirfst du einen mehrstufigen Vorgang, der eine Datenbank aktualisiert und eine externe API aufruft?

Speichere zunächst den Auftrag samt Nutzdaten und Status `PENDING` in einer lokalen Transaktion. Schließe sie vor dem Netzwerkaufruf ab, damit du keine Sperren und Poolverbindungen während der Wartezeit belegst. Rufe den Anbieter mit einem stabilen Idempotenzschlüssel auf, etwa aus der Vorgangs-ID. Unterstützt der Anbieter diesen Mechanismus, lösen Wiederholungen innerhalb seines Gültigkeitsfensters keine zweite Abbuchung aus. Wechsle bei bestätigtem Erfolg zu `COMPLETED` und bei endgültigem Scheitern zu `FAILED`; gleiche bei Bedarf bereits ausgeführte Schritte aus. Nach einem Timeout kann die Ausführung erfolgreich gewesen sein. Halte den Zustand als ungeklärt fest und kläre ihn im Hintergrund über den Statusendpunkt oder einen zulässigen Wiederholungsversuch mit demselben Schlüssel.

use uuid::Uuid;

pub async fn process_external_charge(pool: &sqlx::PgPool, user_id: Uuid, amount: i64) -> Result<(), String> {
    let payment_id = Uuid::new_v4();
    let idempotency_key = format!("pay_{}", payment_id);

    // 1. Commit intent locally before making external network calls
    sqlx::query!(
        "INSERT INTO payments (id, user_id, amount, status, idempotency_key) VALUES ($1, $2, $3, 'PENDING', $4)",
        payment_id, user_id, amount, idempotency_key
    )
    .execute(pool)
    .await
    .map_err(|e| e.to_string())?;

    // 2. Call external API with timeout and idempotency key
    let client = reqwest::Client::new();
    let api_result = client.post("https://api.payment.com/v1/charges")
        .header("Idempotency-Key", &idempotency_key)
        .json(&serde_json::json!({ "amount": amount }))
        .timeout(std::time::Duration::from_secs(5))
        .send()
        .await;

    // 3. Update state based on outcome; handle timeouts via reconciliation
    match api_result {
        Ok(resp) if resp.status().is_success() => {
            sqlx::query!("UPDATE payments SET status = 'SUCCESS' WHERE id = $1", payment_id)
                .execute(pool)
                .await
                .map_err(|e| e.to_string())?;
        }
        Ok(_) => {
            sqlx::query!("UPDATE payments SET status = 'FAILED' WHERE id = $1", payment_id)
                .execute(pool)
                .await
                .map_err(|e| e.to_string())?;
        }
        Err(_) => {
            sqlx::query!("UPDATE payments SET status = 'REQUIRES_RECONCILIATION' WHERE id = $1", payment_id)
                .execute(pool)
                .await
                .map_err(|e| e.to_string())?;
        }
    }
    Ok(())
}
Diese Frage mit einem KI-Coach beantworten

15Wie würden Sie ein mandantenfähiges Rust-Backend mit strikter Datenisolierung und dynamischem Routing entwerfen?

Der Entwurf eines mandantenfähigen Rust-Backends umfasst drei Schlüsselbereiche: Mandantenidentifikation, Strategie zur Datenisolierung und dynamisches Routing. 1. **Mandantenextraktion & Kontextpropagierung**: Eine HTTP-Middleware (z.B. in `Axum` oder `Actix-web`) ermittelt den angefragten Mandanten aus verifizierten Claims oder Routing-Eingaben und prüft, ob der authentifizierte Aufrufer dazu berechtigt ist. Ein vom Client gelieferter Header oder eine Subdomain allein ist keine vertrauenswürdige Autorisierungsgrundlage. Sie erstellt einen streng typisierten `TenantContext`, der in Request-Extensions gespeichert wird. Handler verwenden Rust-Typ-Extraktoren, um sicherzustellen, dass Geschäftslogik ohne einen gültigen Mandantenkontext nicht ausgeführt werden kann. 2. **Strategie zur Datenisolierung**: * **Gemeinsame Datenbank mit Spalten-/Zeilen-Level-Security (RLS)**: Alle Mandanten teilen sich Tabellen mit einer `tenant_id`-Spalte. Die Isolation wird über PostgreSQL RLS (Sicherheit auf Zeilenebene) mit verbindungsspezifischen Sitzungsvariablen erzwungen, etwa durch ein parametrisiertes `SELECT set_config('app.tenant_id', $1, true)` innerhalb der Transaktion. Dafür braucht es getestete RLS-Richtlinien und Datenbankrollen, die diese nicht umgehen können. Ein Query-Wrapper allein ist dagegen eine von der Anwendung durchgesetzte Schutzmaßnahme. Setzen Sie den transaktionslokalen Kontext auf jeder aus dem Pool entnommenen Verbindung und testen Sie Pool-Wiederverwendung sowie Hintergrundjobs. * **Schema pro Mandant**: Mandanten teilen sich eine Datenbankinstanz, haben aber isolierte Schemata; das Routing verwendet sicher quotierte oder über eine Zulassungsliste geprüfte Schema-Bezeichner und einen auf den jeweiligen Geltungsbereich begrenzten `search_path`. Schemata allein ersetzen weder Berechtigungen noch Autorisierung. * **Datenbank pro Mandant**: Mandanten haben separate Datenbanken. Der Dienst verwaltet eine Pool-Registry (z.B. `Arc<DashMap<TenantId, PgPool>>` oder einen LRU-Pool-Cache (Verdrängung der am längsten nicht genutzten Einträge)), um dynamisch Verbindungspools aufzulösen. 3. **Leckage-Prävention**: Um mandantenübergreifende Kontamination in asynchronen `Tokio`-Anwendungen zu vermeiden, muss der Mandantenkontext explizit über Task-Grenzen hinweg übergeben werden und darf nicht in globalem Zustand oder Thread-Local Storage (`thread_local!`) gespeichert werden, da dies bei Migrationen zwischen asynchronen Worker-Threads nicht funktioniert.

use axum::{extract::{FromRequestParts, State}, http::request::Parts, async_trait};
use std::sync::Arc;
use dashmap::DashMap;
use sqlx::PgPool;

#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct TenantId(pub String);

#[derive(Clone)]
pub struct AppState {
    pub pool_registry: Arc<DashMap<TenantId, PgPool>>,
}

#[derive(Clone)]
pub struct TenantContext {
    pub tenant_id: TenantId,
    pub pool: PgPool,
}

#[async_trait]
impl FromRequestParts<AppState> for TenantContext {
    type Rejection = (axum::http::StatusCode, &'static str);

    async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, Self::Rejection> {
        let tenant_header = parts.headers.get("X-Tenant-ID")
            .and_then(|v| v.to_str().ok())
            .ok_or((axum::http::StatusCode::BAD_REQUEST, "Missing tenant ID"))?;
        
        let tenant_id = TenantId(tenant_header.to_string());
        
        let pool = match state.pool_registry.get(&tenant_id) {
            Some(p) => p.value().clone(),
            None => return Err((axum::http::StatusCode::NOT_FOUND, "Tenant database pool not found")),
        };

        Ok(TenantContext { tenant_id, pool })
    }
}
Diese Frage mit einem KI-Coach beantworten

Übe Interviews auf deinem Smartphone: Rust

Antworte laut und erhalte KI-Feedback in der Android-App.

EngineerSpeak bei Google Play herunterladen
Zum Download scannen