1
 
 
Account
In your account you can view the status of your application, save incomplete applications and view current news and events
August 18, 2026

Rewriting OTTO in Rust
oxidizing our codebase, one service at a time

What is the article about?

In this article, you'll learn why one of our engineering teams gradually migrated its AWS Lambda functions and microservices to Rust and discovered that the famous engineering mantra "Rewrite it in Rust" was more than just a meme. The focus lies on the programming language’s advantages in terms of error handling, performance, resource requirements, and developer experience, as well as a comparison with Kotlin, TypeScript, and Go. The article also shows how Rust evolved from an initial experiment to a production-ready backend standard, the challenges that arose along the way, and the lessons learned from migration, pair programming, and collaborative knowledge-building .

You can read more about Rust at OTTO here:

Where do we come from?

FT9 is one of many development teams responsible for specific areas of otto.de and the app, known as "verticals". For this blog post, the "Benefit" vertical is particularly relevant, as this is where we develop customer benefits. It is our team’s core product. All the authors of this post work in FT9.

Throughout their customer journey, our customers encounter customer benefits at various points. These can reduce the price of a product, thereby helping customers save money when shopping. For OTTO and its marketplace partners, customer benefits also offer the opportunity to differentiate products from the competition and position them strategically. To ensure customers actually take advantage of these benefits, we display them at many different touchpoints. Some of these areas are personalised. For example, we sort customer benefits based on a score or filter them to match the products displayed in a search. To meet these requirements, we run microservices and Lambdas on AWS. Our microservices process an average of around 5,000 requests per second. Wherever possible, the client or an upstream proxy caches the requests. For the remaining requests, our goal is to achieve a p95 (95th percentile latency) response time of under ten milliseconds.

We started out with classic Spring Boot microservices, initially in Java and later in Kotlin. The infrastructure and monitoring were implemented in Python. Other teams frequently forked these solutions, which meant that new applications could be brought into production quickly.

Over the years, our technology stack has expanded, and Lambda functions have been added to our services. To this end, we trialled several languages: Python, Java, Kotlin and TypeScript. Each presented its own challenges. We lacked the necessary depth of experience with Python within the team. Java and Kotlin failed to meet our performance requirements, particularly during cold starts. With TypeScript, we faced a different problem: Serverless, our build tool for TypeScript Lambdas, released version 4 as a paid upgrade and, at the same time, announced the end of support for version 3.

Why Rust?

So why Rust and not one of the many alternatives on the market? Our decision was based on a comparison of various technologies and the requirements of our serverless architecture.

The search for the right language

Kotlin was the obvious choice at first, as our existing applications were already written in it. However, when it came to lambdas, it was the JVM tooling in particular that held us back. Bundling Kotlin lambdas reliably was cumbersome and error-prone. Furthermore, whilst we had a great deal of experience with Kotlin and Spring Boot within the team, we had significantly less experience with the ecosystem outside of Spring. We also tested Spring Boot with lambdas, but rejected this approach due to build issues and very high latencies. AWS SnapStart was not yet available in eu-central-1 at that time.

We considered Go as a second language. However, the language differs significantly from the concepts and conventions we have come to value in Kotlin. In particular, we found ourselves lacking a similarly expressive type system, null safety comparable to Kotlin’s, and a balanced mix of different programming paradigms, such as in the declarative handling of iterators. Continuing to use TypeScript was also not an attractive option for us. The tooling is suboptimal and fragmented, and the language also has several drawbacks: types are not enforced at runtime, the fragmentation into ESM and CJS creates additional complexity, and other peculiarities make it difficult to use.

As it happened, several developers in the team had already been exploring Rust in their spare time. This put Rust – which was still rather niche for Lambdas at the time – on our radar, initially out of purely hypothetical interest. However, following a showcase on Rust Lambdas at the Serverless Summit, as well as a number of practical experiments and tests, it quickly became clear that Rust not only works for Lambdas, but also fits our use cases and team preferences very well.

What makes Rust attractive for lambdas

So what exactly makes Rust attractive for Lambdas? First, a technical overview: Rust is a statically and strongly typed, compiled programming language which, much like Kotlin, combines imperative and functional programming. Unlike languages such as Kotlin, however, Rust does not come with a runtime environment and does not require a garbage collector. Instead, memory safety is ensured by rules that the compiler checks.

Rust is compiled to native machine code via LLVM and requires no runtime. This contributes to small binaries, short start-up times and low resource consumption. In the linked Lambda benchmark, Rust is faster and more resource-efficient than the alternatives mentioned. This combination is particularly interesting for Lambdas because their costs depend on runtime and allocated computing resources. Rust therefore reduces both execution time and resource requirements.

The strong performance and efficiency weren't the primary reasons for our decision, but a welcome bonus. Rust’s powerful type system is far more important to us. The absence of null pointers is particularly relevant. It was a real no-brainer for us, as it plays a crucial role in ensuring the correctness of the code. Furthermore, Rust maps potential error states as enums within the type system, thereby preventing nasty surprises caused by unhandled exceptions at runtime. Such exceptions are a major weakness of the Kotlin type system, as they render critical control flow invisible to developers. Rust makes the implicit explicit: if an error can occur, it must be handled.

Last but not least, Rust’s tooling is first-class. Build and dependency management, linters, formatters and test runners are closely integrated. This means we rarely have to switch between different tools and can focus more on what really matters: writing code. In addition, Cargo Lambda provides dedicated Lambda tooling that makes it easier to build, run locally and deploy Lambdas.

All these insights convinced us to give Rust a go for Lambdas. But isn’t Rust considered a particularly complex language? It’s true that Rust does things quite differently from other languages – partly to enable high performance – and some language constructs may seem challenging at first glance. In our case, however, we mainly write highly abstracted CRUD code, which can also be implemented in Rust without any problems. The syntax remains C-like, and familiar abstractions such as iterators or async/await made it easier for us to get started. Based on our experience, we cannot therefore confirm the widespread preconception that Rust is fundamentally too complex.

Algebraic data types have also helped us to model domain logic more precisely than we could previously in Kotlin. In Rust, more logic can be formally expressed within the type system. This means that invalid states become apparent earlier, and the actual code becomes more reliable.

It has also become apparent recently that Rust is well suited to agentic workflows. The powerful type system and the informative compiler messages provide an agent with rapid, high-quality feedback on whether changes are correct. This creates a short feedback loop: the agent makes a change using the contextual information available in the type system, the compiler and tests provide immediate feedback, and the agent uses this feedback for the next adjustment. The automation of code writing also puts into perspective the widespread perception of Rust as an overly complex language.

Taken together, these characteristics were convincing enough for us to start using Rust productively for our Lambdas, thereby laying the foundation for further services written in Rust.

Adopting Rust in practice

We deliberately introduced Rust incrementally: first in Lambdas, then in long-running services. This allowed us to test the language in a production environment without risking disruption to operations through a big-bang migration. We started with Lambdas because they were small, well-defined and could be iterated on quickly using the Rust toolchain. Initially, we migrated functions that were not business-critical but were complex enough to provide robust insights for future services. At the same time, we reviewed our architecture and tooling. We were able to replace some legacy functions with leaner alternatives. For example, we replaced our in-house JVM job frameworks (for scheduled background processes) with lambdas.. Building on this experience, we ventured into long-running services and gradually implemented endpoints in a Rust service running in parallel. As web frameworks in Rust are more modular and leaner than those in the JVM ecosystem, we initially had to make more decisions ourselves and consciously assemble individual building blocks.

Neue modulare Architektur der Benefit-Plattform in Rust
Schematische Darstellung der neuen modularen Architektur der Benefit-Plattform. Frontend-Services greifen auf Models für aktive Kundenvorteile zu. Ein Model Creator und Importer-Lambdas verarbeiten Daten zwischen einer Benefit-Datenbank und Apache Kafka. Die Komponenten sind über Datenflüsse miteinander verbunden und bilden die Rust-basierte Service-Architektur des Teams ab.

Figure 1: New modular architecture.

What went well?

Our incremental approach has proved its worth. We were initially able to deploy Rust in areas where we could get rapid feedback, and subsequently apply the insights gained to our services without jeopardising operations. The impact was particularly evident in terms of performance, resource usage and start-up times. Our Lambdas start without a lengthy warm-up phase, and our services require fewer resources under load. This enables us to provision additional capacity more quickly during peak loads.

Since the complete migration of our team’s code to Rust, we have had no production incidents attributable to errors in our application code. Rust is a key factor in this: error conditions are visible in the type system, and we identify many issues during compilation rather than during or after deployment. As a result, fewer bugs make it into production, our operational overhead is reduced, and we can iterate more quickly. New code is also easier for us to read and more secure.

Building knowledge together was at least as important as the tooling. We worked through the material together, discussed decisions as a team, and later shared our experiences in OTTO-wide sessions such as Show-and-Tell. Particularly in the early stages, focusing on short feedback cycles really helped us. We combined learning resources such as the Rust Book, Rustlings and 100 Exercises to Learn Rust with production code and pair programming within the team. At first, the compiler felt like an additional adversary. However, it quickly became a tool that highlighted errors early on, thereby providing reassurance. To help new colleagues get up to speed, we translated these experiences into clear team practices. Pair programming and community resources helped us apply these practices to actual code and become productive quickly, even without prior experience of Rust.

From a junior developer’s perspective:
For me, Rust was less about ‘a new syntax’ and more about a new way of thinking at the start. The balance between learning the material, pair programming and making small, self-contained changes to production code was key: I was able to try out concepts straight away, get feedback from the compiler and ask more in-depth questions during pair programming. The exceptionally good Rust documentation and exercises were particularly helpful.

What challenges did we face?

In addition to style and architectural considerations, we encountered other challenges typical of Rust: ownership, lifetimes, and safely working with the borrow checker take time. This is especially true when you want to not only satisfy the compiler but also write clean and efficient code. Over time, however, we became familiar with idiomatic and maintainable patterns. For us, these include Rust’s conversion traits such as "From" and "AsRef", as well as Axum’s "FromRequestParts" trait for handler functions. These allow us to elegantly extract additional information from a request.

One challenge was the transition from a coding style that was initially not very Rust-idiomatic to idiomatic Rust. Our setup was heavily influenced by Spring, Kotlin, and Java. At first, a lot of it felt like a direct translation from our previous stack. Over time, through refactoring, we established common standards: more enums and type modeling, better error handling, and greater use of Rust patterns and features. For example, we agreed to bubble up non-retryable errors using the question mark operator "?" and to use a single, larger error enum instead of scattering multiple small ones throughout the code. At the architectural level, we placed greater emphasis on the Functional Core/Imperative Shell pattern, which separates business logic from technical side effects. This enabled us to clearly separate pure functions – that is, deterministic functions without side effects – from infrastructure code, making the code clearer and easier to test. Our code is now housed in a single monorepo.

Beispiel des Result-Typs in Rust
Codeausschnitt in dunkler Entwicklungsumgebung, der das Rust-Enum Result<T, E> zeigt. Das Enum enthält die Varianten Ok(T) für erfolgreiche Ergebnisse und Err(E) für Fehlerwerte. Kommentare beschreiben die jeweilige Bedeutung der beiden Rückgabewerte.

Figure 2: Result Enum from std::result

The consistent use of Clippy lints also helped us during the transition. Gradually, discussions about the lints led to the establishment of shared team conventions. Rules such as "checked_conversions" or "match_wildcard_for_single_variants"not only clarified style issues but also prevented specific bugs.

From a junior developer’s perspective: Ownership and lifetimes have drastically changed the way I look at code. Instead of hoping that “the compiler will optimize it,” you’re forced to explicitly understand data flow and trade-offs (copying vs. referencing, data lifetimes, concurrency). This is challenging at first, but becomes manageable with targeted deep dives and pair programming. From then on, the compiler feels less like a gatekeeper and more like a safety net.

What have we learned?

Our most important insight is therefore less technical and more organizational in nature: A new language does not have to prove that it works in an isolated experiment. Small, productive components, short feedback cycles, and a step-by-step expansion provide reliable answers more quickly than lengthy discussions about theoretical pros and cons.

At the same time, the myth that Rust is difficult to get started with has been put into perspective for us. Similar to Google, we’ve found that getting started is very doable when learning phases, pairing, and productive tasks are combined effectively. In hindsight, however, we should have scheduled an external review of our practices sooner. That would likely have saved us a few iterations and subsequent refactorings.

From a junior developer’s perspective: The “hard” concepts pay off later on: You develop a stronger sense of security, data flow, and the system’s limitations. Plus, you spot almost all problems very early on. These rapid feedback cycles help with learning.

Where are we now and what's next?

Rust is now our standard for production environments. Both our services and our Lambdas run on it. Its lower resource usage, reliability, and strong developer experience have since convinced other teams as well. As a result, what started as a local technology decision has evolved into a development that extends beyond our team. Several teams are now using Rust. They were won over not only by performance and security, but above all by the strong developer experience, the explicit modeling of errors, and the resulting software quality. This combination of safety, clarity, and rapid feedback makes Rust attractive for production-grade backend systems. We now have both intra-team and cross-team initiatives that make knowledge visible, share experiences, and help new teams get started. These include show-and-tells, Rust initiatives at our annual company-wide Tech Quality Days, and collaboratively developed closed-source libraries.

Of course, we are continuing to evolve our setup. New tooling, new architectural approaches, and better patterns help us make the code safer and more maintainable through refactoring and iterate faster. Next, we want to standardize core business objects from a domain perspective. To this end, we’ve developed a model that maps requirements directly to Rust’s type system and prevents invalid states in the code. For us, Rust has evolved from an experiment to our default production language: in production, across teams, and increasingly as a shared engineering practice. Our experience thus contradicts a common assumption: for backend teams, Rust is productive not in spite of its strict compiler, but precisely because of it.

We hope you enjoyed the article! Feel free to share your thoughts in the comments. And if you’d like to learn more about “Rust at OTTO,” be sure to check out the following article:

Want to join us?

2 people like this.
0No comments yet.
Write a comment
Leave us a comment here and let the authors know what you thought of the article.
Answer to: Reply directly to the topic

Written by

Falk Woldmann
Falk Woldmann
Software Developer
Nico Schmidt
Nico Schmidt
Software Developer
René Hottendorf
René Hottendorf
Software Developer

Similar Articles

Saved!

We want to improve out content with your feedback.

How interesting is this blogpost?

We have received your feedback.