In diesem Artikel erfährst du, warum eines unserer Engineering-Teams seine AWS-Lambdas und Microservices schrittweise auf Rust umgestellt hat und herausfand, dass "Rewrite it in Rust" mehr als nur ein Meme sein kann. Im Fokus stehen die Vorteile der Programmiersprache bei Error-Handling, Performance, Ressourcenbedarf und Developer Experience sowie der Vergleich mit Kotlin, TypeScript und Go. Zudem zeigt der Beitrag, wie Rust vom ersten Experiment zum produktiven Backend-Standard wurde, welche Herausforderungen dabei entstanden und welche Learnings sich aus Migration, Pairing und dem gemeinsamen Wissensaufbau ergeben haben.
Mehr zum Thema Rust bei OTTO lest ihr hier:
FT9 ist eines von vielen Entwicklungsteams, die Teilbereiche von otto.de und der App verantworten, sogenannte Vertikalen. Für diesen Blogbeitrag ist vor allem die Vertikale Benefit relevant, in der wir Kundenvorteile entwickeln. Sie ist das zentrale Produkt unseres Teams. Alle Autoren dieses Beitrags arbeiten in FT9.
Im Laufe ihrer Customer Journey kommen unsere Kund*innen an verschiedenen Stellen mit Kundenvorteilen in Kontakt. Diese können den Preis eines Produkts reduzieren und Kund*innen so beim Einkauf sparen lassen. Für OTTO und seine Marktplatzpartner bieten Kundenvorteile zugleich die Möglichkeit, Produkte vom Wettbewerb abzuheben und gezielt zu positionieren.
Damit Kund*innen die Vorteile tatsächlich wahrnehmen, zeigen wir sie an vielen verschiedenen Touchpoints. Einige dieser Flächen sind personalisiert. Dabei sortieren wir Kundenvorteile zum Beispiel anhand eines Scores oder filtern sie passend zu den Produkten, die in einer Suche angezeigt werden.
Für diese Anforderungen betreiben wir Microservices und Lambdas in AWS. Unsere Microservices verarbeiten im Tagesdurchschnitt rund 5.000 Requests pro Sekunde. Viele Anfragen können durch Caching bereits vom Client oder einem vorgeschalteten Proxy beantwortet werden. Für die verbleibenden Anfragen liegt unser Ziel bei einer Antwortzeit von unter zehn Millisekunden im p95 (95th percentile latency).
Gestartet sind wir mit klassischen Spring-Boot-Microservices, zunächst in Java und später in Kotlin. Infrastruktur und Monitoring waren in Python umgesetzt. Andere Teams forken diese Lösungen häufig, wodurch neue Anwendungen schnell produktiv gehen konnten.
Über die Jahre wurde unser Technologiestack breiter, und zu den Services kamen Lambdas hinzu. Dafür erprobten wir mehrere Sprachen: Python, Java, Kotlin und TypeScript. Jede brachte eigene Schwierigkeiten mit sich. Für Python fehlte uns im Team die nötige Erfahrungstiefe. Java und Kotlin erfüllten vor allem beim Cold Start unsere Performanceanforderungen nicht. Bei TypeScript standen wir vor einem anderen Problem: Serverless, unser Build-Tool für TypeScript-Lambdas, veröffentlichte Version 4 als kostenpflichtiges Upgrade und kündigte zugleich das Ende des Supports für Version 3 an.
Warum also Rust und nicht eine der vielen Alternativen am Markt? Unsere Entscheidung entstand aus dem Vergleich verschiedener Technologien und den Anforderungen unserer Serverless-Architektur.
Kotlin lag zunächst nahe, weil unsere bestehenden Anwendungen bereits darin geschrieben waren. Für Lambdas bremste uns jedoch vor allem das JVM-Tooling aus. Kotlin-Lambdas zuverlässig zu bundlen war umständlich und fehleranfällig. Außerdem hatten wir im Team viel Erfahrung mit Kotlin und Spring Boot, aber deutlich weniger mit dem Ökosystem außerhalb von Spring. Spring Boot auf Lambdas testeten wir ebenfalls, verwarfen den Ansatz jedoch wegen Problemen beim Build und sehr hoher Latenzen. AWS SnapStart war zu diesem Zeitpunkt in eu-central-1 noch nicht verfügbar.
Als zweite Sprache betrachteten wir Go. Die Sprache weicht jedoch stark von den Konzepten und Konventionen ab, die wir an Kotlin schätzen gelernt haben. Uns fehlten insbesondere ein ähnlich ausdrucksstarkes Typsystem, eine mit Kotlin vergleichbare Null-Safety und der ausgewogene Mix verschiedener Programmierparadigmen, etwa bei der deklarativen Verarbeitung von Iteratoren.
Auch TypeScript weiterzuverwenden war für uns nicht attraktiv. Das Tooling ist suboptimal und fragmentiert, zudem bringt die Sprache mehrere Nachteile mit sich: Typen werden zur Laufzeit nicht erzwungen, die Fragmentierung in ESM und CJS erzeugt zusätzliche Komplexität und weitere Besonderheiten erschweren den Einsatz.
Zufälligerweise hatten sich mehrere Entwickler*innen im Team bereits privat mit Rust beschäftigt. Dadurch rückte die damals für Lambdas noch eher nischige Sprache in unseren Blick, zunächst aus rein hypothetischem Interesse. Nach einem Showcase zu Rust-Lambdas auf dem Serverless Summit sowie einigen praktischen Experimenten und Tests zeigte sich jedoch schnell: Rust funktioniert nicht nur für Lambdas, sondern passt auch sehr gut zu unseren Use Cases und Teampräferenzen.
Was macht Rust also konkret für Lambdas attraktiv? Zunächst eine technische Einordnung: Rust ist eine statisch und stark typisierte, kompilierte Programmiersprache, die ähnlich wie Kotlin imperative und funktionale Programmierung verbindet. Anders als Sprachen wie Kotlin bringt Rust jedoch keine Laufzeitumgebung mit und benötigt keinen Garbage Collector. Stattdessen gewährleisten Regeln, die der Compiler prüft, die Speichersicherheit.
Rust wird über LLVM zu nativem Maschinencode kompiliert und benötigt keine Runtime. Das trägt zu kleinen Artefakten, kurzen Startzeiten und geringem Ressourcenbedarf bei. Im verlinkten Lambda-Benchmark ist Rust schneller und ressourceneffizienter als die genannten Alternativen. Für Lambdas ist diese Kombination besonders interessant, weil ihre Kosten von Laufzeit und allokierten Rechenressourcen abhängen. Rust reduziert damit sowohl die Ausführungszeit als auch den Ressourcenbedarf.
Die gute Performance und Effizienz waren für unsere Entscheidung nicht ausschlaggebend, aber eine willkommene Ergänzung. Wesentlich wichtiger ist für uns Rusts mächtiges Typsystem. Besonders relevant ist die Abwesenheit von Null-Pointern. Sie war für uns ein echter No-Brainer, weil sie entscheidend zur Korrektheit des Codes beiträgt. Darüber hinaus bildet Rust mögliche Fehlerzustände als Enums im Typsystem ab und verhindert damit böse Überraschungen durch unbehandelte Exceptions zur Laufzeit. Solche Exceptions sind eine wesentliche Schwäche des Kotlin-Typsystems, weil sie kritischen Kontrollfluss für Entwickler*innen unsichtbar machen. Rust macht das Implizite explizit: Wenn ein Fehler auftreten kann, muss er behandelt werden.
Nicht zuletzt ist das Rust-Tooling erstklassig. Build- und Dependency-Management, Linter, Formatter und Test-Runner sind eng aufeinander abgestimmt. Dadurch müssen wir seltener zwischen verschiedenen Werkzeugen wechseln und können uns stärker auf das Wesentliche konzentrieren: Code zu schreiben. Ergänzend gibt es mit Cargo Lambda ein dediziertes Lambda-Tooling, das das Bauen, lokale Starten und Deployen von Lambdas erleichtert.
All diese Erkenntnisse überzeugten uns davon, Rust für Lambdas auszuprobieren. Doch gilt Rust nicht als besonders komplexe Sprache? Tatsächlich macht Rust einiges anders als andere Sprachen, unter anderem um hohe Performance zu ermöglichen, und manche Sprachkonstrukte wirken auf den ersten Blick anspruchsvoll. In unserem Fall schreiben wir jedoch überwiegend hoch abstrahierten CRUD-Code, der sich auch in Rust problemlos umsetzen lässt. Die Syntax ist weiterhin C-like, und bekannte Abstraktionen wie Iteratoren oder async/await erleichterten uns den Einstieg. Das verbreitete Vorurteil, Rust sei grundsätzlich zu komplex, können wir daher aus unserer Erfahrung nicht bestätigen.
Algebraische Datentypen haben uns außerdem dabei geholfen, Domänenlogik präziser zu modellieren als zuvor in Kotlin. In Rust lässt sich mehr Logik formal im Typsystem ausdrücken. Dadurch werden ungültige Zustände früher sichtbar und der eigentliche Code sicherer.
In letzter Zeit zeigt sich außerdem, dass Rust gut für Agentic Workflows geeignet ist. Das mächtige Typsystem und die aussagekräftigen Compiler-Messages geben einem Agenten schnell hochwertiges Feedback dazu, ob Changes korrekt sind. So entsteht ein kurzer Feedback Loop: Der Agent nimmt mithilfe der im Typsystem verfügbaren Kontextinformationen einen Change vor, Compiler und Tests liefern unmittelbar Feedback, und der Agent nutzt dieses Feedback für die nächste Anpassung. Die Automatisierung des Codeschreibens relativiert zudem das verbreitete Bild von Rust als übermäßig komplexer Sprache.
Zusammengenommen waren diese Eigenschaften für uns überzeugend genug, Rust produktiv für unsere Lambdas einzusetzen und damit den Grundstein für weitere Services in Rust zu legen.
Wir haben Rust bewusst inkrementell eingeführt: zuerst in Lambdas, später in Long-Running Services. So konnten wir die Sprache im produktiven Umfeld erproben, ohne den Betrieb durch eine Big-Bang-Migration zu riskieren. Den Einstieg bildeten Lambdas, weil sie klein, gut abgegrenzt und mit dem Rust-Tooling schnell iterierbar waren. Zuerst migrierten wir Funktionen, die nicht geschäftskritisch waren, aber genug Komplexität mitbrachten, um belastbare Erkenntnisse für spätere Services zu liefern.
Parallel dazu hinterfragten wir unsere Architektur und unser Tooling. Einige Legacy-Funktionen konnten wir durch schlankere Alternativen ersetzen. Selbst entwickelte JVM-Job-Frameworks (für zeitgesteuerte Hintergrundprozesse) lösten wir beispielsweise durch Lambdas ab. Nach diesen Erfahrungen wagten wir uns an langlebige Services und implementierten Endpunkte schrittweise in einem parallel betriebenen Rust-Service. Da Webframeworks in Rust modularer und schlanker als im JVM-Ökosystem sind, mussten wir anfangs mehr Entscheidungen selbst treffen und einzelne Bausteine bewusst zusammensetzen.
Abbildung 1: Neue modulare Architektur.
Unser inkrementeller Ansatz hat sich bewährt. Wir konnten Rust zunächst dort einsetzen, wo es schnelles Feedback gibt, und die Erkenntnisse anschließend auf unsere Services übertragen, ohne den Betrieb zu riskieren. Besonders sichtbar wurde der Effekt bei Performance, Ressourcenbedarf und Startzeiten. Unsere Lambdas starten ohne lange Warm-up-Phase, und unsere Services benötigen unter Last weniger Ressourcen. Dadurch können wir bei Lastspitzen schneller zusätzliche Kapazität bereitstellen.
Seit der vollständigen Migration unseres Team-Codes zu Rust hatten wir keine Production Incidents, die auf Fehler in unserem Anwendungscode zurückzuführen waren. Rust ist dafür ein ausschlaggebender Faktor: Fehlerzustände sind im Typsystem sichtbar, und viele Probleme finden wir bereits beim Kompilieren statt erst während oder nach dem Deployment. Dadurch gelangen weniger Bugs in die Produktion, unser Betriebsaufwand sinkt und wir können schneller iterieren. Neuer Code ist für uns außerdem leichter lesbar und sicherer.
Mindestens ebenso wichtig wie das Tooling war der gemeinsame Wissensaufbau. Wir arbeiteten Inhalte zusammen durch, diskutierten Entscheidungen im Team und teilten unsere Erfahrungen später in OTTO-weiten Formaten wie Show-and-Tell. Gerade in der Anfangsphase half uns der Fokus auf kurze Feedbackzyklen. Wir kombinierten Lernressourcen wie das Rust Book, Rustlings und 100 Exercises to Learn Rust mit Production-Code und Pairing im Team. Am Anfang fühlte sich der Compiler wie ein zusätzlicher Gegner an. Schnell wurde er aber zu einem Werkzeug, das Fehler früh sichtbar machte und dadurch Sicherheit gab. Für das Onboarding neuer Kolleg*innen haben wir diese Erfahrungen in klare Team-Practices übersetzt. Pairing und Community-Ressourcen halfen dabei, die Practices im konkreten Code anzuwenden und auch ohne Rust-Vorerfahrung schnell produktiv zu werden.
Aus Junior-Perspektive: Für mich war Rust am Anfang weniger „eine neue Syntax“ und mehr ein neues Denken. Der Wechsel zwischen Lerninhalten, Pairing und kleinen, abgeschlossenen Production-Änderungen war dabei entscheidend: Ich konnte Konzepte direkt ausprobieren, Feedback vom Compiler bekommen und im Pairing tiefere Fragen stellen. Besonders geholfen haben die außergewöhnlich guten Rust-Docs und Übungen.
Neben Stil- und Architekturfragen begegneten uns weitere für Rust typische Herausforderungen: Ownership, Lifetimes und der sichere Umgang mit dem Borrow Checker brauchen Zeit. Das gilt besonders dann, wenn man nicht nur den Compiler zufriedenstellen, sondern zugleich sauberen und effizienten Code schreiben möchte. Mit der Zeit lernten wir jedoch idiomatische und wartbare Patterns kennen. Dazu zählen für uns Rusts Conversion-Traits wie From und AsRef sowie Axums FromRequestParts-Trait für Handler-Funktionen. Damit lassen sich zusätzliche Informationen elegant aus einem Request extrahieren.
Eine Herausforderung war der Weg von einem zunächst wenig Rust-idiomatischen Code-Stil hin zu idiomatischem Rust. Unser Setup war stark von Spring, Kotlin und Java geprägt. Vieles fühlte sich anfangs wie eine direkte Übersetzung aus unserem bisherigen Stack an. Über Refactorings etablierten wir mit der Zeit gemeinsame Standards: mehr Enums und Typmodellierung, besseres Error-Handling sowie eine stärkere Nutzung von Rust-Patterns und Features. So verständigten wir uns darauf, non-retryable Errors mit dem Fragezeichenoperator ? hochzububbeln und ein größeres Error-Enum zu verwenden, statt mehrere kleine über den Code zu verteilen. Auf Architektur-Ebene setzten wir stärker auf das Functional-Core-/Imperative-Shell-Pattern, bei dem fachliche Logik von technischen Nebeneffekten getrennt wird. Dadurch konnten wir Pure Functions, also deterministische Funktionen ohne Seiteneffekte, klar von Infrastrukturcode trennen und den Code übersichtlicher und besser testbar machen. Unser Code liegt heute in einem Monorepo.
Abbildung 2: Result Enum aus std::result
Beim Übergang half uns außerdem die konsequente Nutzung von Clippy-Lints. Aus den Diskussionen über die Lints entstanden nach und nach gemeinsame Teamkonventionen. Regeln wie checked_conversions oder match_wildcard_for_single_variants klärten dabei nicht nur Stilfragen, sondern verhinderten auch konkrete Bugs.
Aus Junior-Perspektive: Ownership und Lifetimes haben meinen Blick auf Code stark verändert. Statt zu hoffen, dass „der Compiler es schon optimiert“, wird man gezwungen, Datenfluss und Trade-offs (Kopieren vs. Referenzieren, Lebensdauer von Daten, Nebenläufigkeit) explizit zu verstehen. Das ist anfangs herausfordernd, wird mit gezielten Deep-Dives und Pairing aber greifbar. Ab da fühlt sich der Compiler weniger wie ein Gatekeeper an und mehr wie ein Sicherheitsnetz.
Unsere wichtigste Erkenntnis ist deshalb weniger technischer als organisatorischer Natur: Eine neue Sprache muss nicht in einem isolierten Experiment beweisen, dass sie funktioniert. Kleine produktive Komponenten, kurze Feedbackzyklen und ein schrittweiser Ausbau liefern schneller belastbare Antworten als lange Diskussionen über theoretische Vor- und Nachteile.
Gleichzeitig hat sich der Mythos vom schwierigen Rust-Einstieg für uns relativiert. Ähnlich wie Google haben wir erlebt, dass der Einstieg sehr gut machbar ist, wenn Lernphasen, Pairing und produktive Aufgaben sinnvoll kombiniert werden. Rückblickend hätten wir allerdings früher ein externes Review unserer Practices eingeplant. Das hätte uns vermutlich einige Iterationen und spätere Refactorings erspart.
Aus Junior-Perspektive: Die „harten“ Konzepte zahlen sich später aus: Man bekommt ein stärkeres Gefühl für Sicherheit, Datenfluss und Grenzen des Systems. Außerdem erkennt man fast alle Probleme sehr früh. Diese schnellen Feedbackzyklen helfen beim Lernen.
Rust ist heute unser Standard im Produktivbetrieb. Sowohl unsere Services als auch unsere Lambdas laufen darin. Der geringere Ressourcenbedarf, das stabile Verhalten und die starke Developer Experience haben inzwischen auch weitere Teams überzeugt. Damit ist aus einer lokalen Technologieentscheidung eine Entwicklung geworden, die über unser Team hinauswirkt.
Inzwischen setzen mehrere Teams Rust ein. Überzeugt haben sie nicht nur Performance und Security, sondern vor allem die starke Developer Experience, die explizite Modellierung von Errors und die daraus resultierende Softwarequalität. Diese Kombination aus Safety, Verständlichkeit und schnellem Feedback macht Rust für produktive Backend-Systeme attraktiv. Mittlerweile gibt es teaminterne und teamübergreifende Formate, die Wissen sichtbar machen, Erfahrungen teilen und neuen Teams den Einstieg erleichtern. Dazu gehören Show-and-Tells, Rust-Initiativen bei unseren jährlichen unternehmensweiten Tech Quality Days und gemeinsam entwickelte Closed-Source-Libraries.
Natürlich entwickeln wir unser Setup weiter. Neues Tooling, neue Architekturansätze und bessere Patterns helfen uns, den Code durch Refactorings sicherer und wartbarer zu machen und schneller zu iterieren. Als Nächstes wollen wir zentrale Business-Objekte fachlich vereinheitlichen. Dafür haben wir ein Modell entwickelt, das die Anforderungen direkt in Rusts Typsystem abbildet und illegale Zustände im Code unmöglich macht. Rust ist bei uns vom Experiment zum produktiven Standard geworden: im Betrieb, über Teams hinweg und zunehmend als gemeinsame Engineering-Praxis. Damit widerspricht unsere Erfahrung einer verbreiteten Annahme: Für Backend-Teams ist Rust nicht trotz seines strengen Compilers produktiv, sondern gerade deswegen.
Wir hoffen, dass euch der Artikel gefallen hat! Lasst uns gern eure Meinung in den Kommentaren da. Und wenn ihr noch mehr über das Thema „Rust bei OTTO“ erfahren wollt, schaut gern auch in den folgenden Artikel rein:
Lust, bei uns zu arbeiten?

We have received your feedback.