Tech stack at amo
Background
We strive to build AAA premium mobile products with an emphasis on design, performance and snappiness and have a history of building those on ambitious timelines while maintaining a high level of craft and a low number of bugs.
This requires making subtle tradeoffs in engineering practices between short and long term decisions, deep focus on performance whether on client (startup time, animation snappiness, wow effect) or backend (low-latency) and close to zero compromise in the quality of foundational platform and framework components.
The team
The amo engineering team is currently 35 people. Mostly generalist programmers with at least one strong specialty over the past few years. We believe that great software needs a decent mix of generalists and specialists to cover the whole spectrum of programming practices.
We all work very closely with product and design and put a lot of emphasis on having the user at the center of all our technical thinking, pushing the boundaries of what could be (more) awesome for the end user.
We make the extra effort of defining, dividing and sizing technical projects and use Linear to communicate those with the rest of the team as well as to track progress within the team. We value rigor, pragmatism and performance in all our technical decisions.
Having a team of seasoned programmers greatly helps in that regard. Since we are working in a single monorepo, we encourage one another to cross language boundaries when it feels right. We believe that exposure to different environments makes you a better programmer.
The stack
We’ve found success going deep in code sharing across the entire stack throughout our past experiences. As code sharing is simpler in a monorepo, we moved in that direction after many years of painful segregated code sharing practices. amo is now one single monorepo containing all projects and built using the same build system: Bazel (an open-source port of Google Blaze).
iOS
We designed our iOS app architecture to be highly modular. To this end, we use Bazel to build a large number of small Swift libraries linked statically into the final binary.
We make extensive use of dependency injection in conjunction with api and implementation submodules to minimize module cache invalidation and ensure proper encapsulation. This way we are able to guarantee that touching implementations will not trigger recompilation of other implementation modules, only that of the final target, which in most cases can leverage incremental compilation and ensure decent build times even with hundreds of modules.
Additionally, we put a big emphasis on app infrastructure components such as Navigation, Scheduling, Instrumentation, Resource Attribution and make feature development as laser-focused as possible while ensuring strong foundations.
We use UIKit primarily given the maturity, performance, versatility and the high level of customization of our UI components. It does not, however, prevent us from using SwiftUI when appropriate. In carefully chosen components, we also leverage Metal and write our own shaders to unlock unprecedented performance and graphic capabilities.
We have a very careful approach when it comes to using third-party libraries but choose to rely heavily on RxSwift as a communication layer between the data model and the UI (see more in Mobile Infrastructure) and Swinject for dependency injection while we wait for a more versatile compile-safe approach to emerge from the community (or for us to build it ourselves).
Android
Our Android architecture has been designed to match our needs for scalability, parallelization, and reusability. The codebase is split into a large number of independent modules built with Gradle. Our modularization strategy emphasizes low coupling and high cohesion. Thanks to Metro for dependency injection and the use of apivs implementation submodules, we minimize module cache invalidation and reduce build times. We keep an eye on Bazel to maintain consistency within the project and further optimize our build system.
Based on our experience building one of the most delightful Android apps in the past, we are building foundational app infrastructure components to tackle common issues like navigation, instrumentation, resource management, attribution, etc. We make extensive use of Jetpack libraries and leverage low-level APIs to make the best use of the Android platform.
We use Compose as our main UI toolkit along with the MVVM architecture. It enables us to write less code, ship faster and simplify UI development. In some cases we do allow ourselves to switch back to the original UI toolkit. In carefully chosen components, we also leverage AGSL and write our own shaders to unlock unprecedented performance and graphic capabilities.
Mobile Infrastructure
One interesting choice we’ve decided to stick to throughout the years is to have the app infrastructure (networking, authentication, data synchronization and persistence, feature data backends, etc.) done using the same technology as the backend (in Rust, see more in Production) and shared across iOS and Android.
This allows for reusing a lot of code between the backend and the app (models, validators, networking components, etc.) that we think offers the advantage of letting engineers who are used to working with data management and networking do that on the client as well as the backend. Because we use a monorepo, this shared component is nothing more than another Swift or Kotlin library linking to many other Rust library targets that the final app target then depends on (Bazel makes all this rather simple).
One cool thing we’ve done is that the entire interface communicating with Rust behind the scenes is code generated, hiding all the FFI complexities. An extra perk of having everything in a monorepo with Bazel is that any piece of code that needs to behave exactly the same on iOS and Android can be done in Rust once and have its Swift and Kotlin function interface generated.
Web
Our web platform extends our mobile-first approach to the browser, maintaining the same emphasis on design, performance, and user experience. Most of our web applications are built with Next.js (App Router), TypeScript, and Bazel within our monorepo, ensuring consistent, hermetic builds and excellent caching.
Web Apps
We’re building full-featured web experiences for Sugar and Bump that match the quality of our iOS and Android apps. This is an ambitious greenfield project that requires reimagining our mobile-first products for the web while maintaining the same level of polish, performance, and user delight.
The challenge involves leveraging our shared Rust infrastructure (the same codebase powering our mobile apps) through WebAssembly and gRPC, while creating web-native experiences that take advantage of the browser’s unique capabilities. We’re building rich, interactive interfaces that handle real-time updates, complex state management, and smooth animations, all while maintaining the snappy and responsive feel that defines our mobile products.
Internal Tooling
Our internal tooling, a backoffice and multiple operational platforms powering the business, is where the entire company runs day to day. Built with domain-driven design principles, these tools sit on top of the same Rust backend as our apps, through gRPC (with Protocol Buffer-generated TypeScript bindings) and WebAssembly for performance-critical work like large-scale map visualizations. The web team works end to end across the stack, from the data model all the way to the pixels.
The scope is wide: resilient, resumable workflows orchestrated with Restate for operations spanning hours or days, data-heavy tools querying millions of records on BigQuery, AI-powered content pipelines and more. Each of these is a product in its own right, held to the same bar as our consumer apps, with a lot of ownership left to the engineers building them. It’s a rare playground for engineers who enjoy owning complex systems and seeing their impact across the whole company.
Production
We’re on Google Cloud Platform and aim to be multi-cloud soon enough. Infrastructure as code (IaC) is used to keep our production environment versioned, secure and reproducible. Plus it enables our developers to release as fast as possible, and as many times as necessary. Pulumi is our tool of choice for now, but we’re open to trying new things as the environment evolves.
Our backend architecture is designed to be rapidly adaptable. Our services are designed to work alongside one another in a single binary, as a monolith. As the system grows, we can move a service to its own deployment and scale it automatically without wasting resources or creating unnecessary duplicates (unused but instantiated connection pools, etc.).
We have the freedom to move between microservices and larger services within our infrastructure as needed. Our experience has taught us that migration will consume most of our time in the future. We’ve embraced that reality and made architectural decisions and adopted practices that make the migration process as painless as possible.
Language
We use Rust as our primary programming language. It may be surprising, but we didn’t choose Rust primarily for “performance”, “memory safety” or “fearless concurrency”. Those are huge perks and clearly reinforce our choice, but the main reason is the unique combination of being able to iterate AND grow quickly (while keeping a sane codebase).
With features such as sum types, pattern matching, strong typing, traits and a functional approach, a lot of problems become simpler to tackle. Landing a large-scale refactoring is not as scary anymore since the compiler has our backs. Scaling, both in terms of headcount and codebase size, without compromising the quality and core team integrity is also eased by the compiler and the tooling.
Newcomers can be confident from day one, since the compiler checks a lot of things statically. rust-analyzer acts as a guide in the codebase and provides auto tips and tricks (through clippy) to write better code, enabling reviews based on the substance of code rather than its form.
Monitoring and Alerting
For metrics, tracing, logging, and continuous profiling, we use industry standards such as Prometheus, OpenTelemetry, and Grafana. These tools enable us to guarantee fast and performant deployments, key to providing a flawless, evolving experience to our users. We provide extensive training on these techniques to everyone doing backend development at amo.
Databases
We’ve been using ScyllaDB (a monstrously fast drop-in replacement for Cassandra) since the very beginning and are still convinced by it. We learned that in cases where eventual consistency is fine, a (really) fast database is an invaluable asset in your toolset.
When we have to deal with a very large number (up to millions) of requests per second, ScyllaDB is our tool of choice. When consistency is required, we choose PostgreSQL. We almost always plug it via CDC into Redpanda (a 10x faster Kafka-compatible data streaming platform), to provide us, via a single write, with a single source of truth for all our data. For example, for some use cases, we’re leveraging it to build up in-memory storage, for simplicity, speed and near real-time caching.
This stack provides us with three different options, each of which has its own advantages and drawbacks, to meet almost every requirement we have. However, we are always searching for new ways to reach our goals.
Data Platform
We consider the data we produce one of our most precious resources. Not only for gaining greater insights about our users and how they use our products but also to be able to run jobs that then produce structured data that we can in turn use to power data-driven features.
Our internal data and analytics platform relies primarily on Protocol Buffers for payloads (we make heavy use of custom field options to annotate data with various processing hints, like privacy filters for long-term storage for instance). All those payloads are pushed to Redpanda, either in their own topic or in shared topics (we use a custom envelope and a custom schema registry to handle multiple schemas in a shared topic).
We then use a mix of Apache Beam and custom Rust jobs to process those messages and for some, store them in long-term Google Cloud Storage in Apache Parquet file format using windowing patterns.
Those Parquet files are processed on a daily basis via a number of Apache Beam (using Google Dataflow) or Apache Spark (using Google Dataproc) jobs depending on use cases. For dashboards and knowledge databases, we rely on BigQuery but are also investigating Materialize.
Continuous Integration
CI is an important part, if not the most important part, of our daily routine. And we chose to invest heavily in it to avoid the usual pitfalls (performance, reproducibility). Using Bazel gives us another advantage here as it provides us with hermetic and reproducible builds which allow us to heavily leverage caching (even in the CI), to achieve very short feedback loops for every developer. Namespace and GitHub Actions are used together to give us flexibility and profiling across our builds.
AI
We ship updates to our backend multiple times a day and to our mobile apps twice a week. This didn’t happen by accident: it’s the result of years of investment in modularization, hermetic builds, fast CI, a single monorepo and strong ownership of what each of us ships. We see AI tools as the next lever to go even faster.
Every engineer has (almost) unlimited access to Claude, Codex or any other tool that makes them faster, and we expect them to use these tools, a lot. Whether it’s to find their way in an unfamiliar part of the codebase, write tests, prototype ideas, ship new features, review code or land large mechanical refactorings, AI is at the center of everything we build.
It turns out that many of the choices described above make our codebase a great fit for these tools. The monorepo gives them the whole picture, from the Rust models down to the Swift and Kotlin interfaces generated from them, so a change can be followed across the entire stack. Small modules with clear api boundaries keep changes scoped and easy to review. Type-safe, compiled languages everywhere mean the compiler catches a lot before anyone reads a line. And with Bazel’s caching, a new worktree is cheap, so everyone can run several agents in parallel.
What doesn’t change is a simple rule: you ship it, you own it. Code goes through the same review no matter who or what wrote it, and if you can’t explain it, it doesn’t merge. That’s also why our interviews are mostly AI-free. AI makes great engineers much faster, but it doesn’t replace judgment, passion and ownership, which is what we’re hiring for.
What’s next
A LOT. We are a fast-evolving company, we make mistakes and work hard to correct them. It’s very probable that some of the above choices will prove wrong in the future and the sooner we find out, the better! We are at the beginning of our journey and past experience has taught us that the most interesting challenges are ahead of us.
Made by friends for friends from Paris