At Snap, we value creativity, fun, and living in the moment as you communicate with your close friends. Our messaging product includes intricate client-side business rules and careful coordination between the client and server to bring the fastest, most reliable, and most fun way to talk with close friends through photos and video. In order to deliver on this, we chose to use C++ to implement a single, consistent messaging experience on both mobile platforms.
Two years ago, we set out to rewrite our entire messaging system to improve performance, cut infrastructure costs, and bring consistency between Android and iOS. Consistency was a major goal of our effort, but consistency can be achieved at several layers. In this post, we will walk through our approach to provide identical behavior on both mobile platforms, reduce engineering cost, and raise the bar on observability and performance.
Consistent behavior and implementation with C++
Consistent behavior was the ultimate product goal of our rewrite. Inconsistent behavior between the two mobile platforms was historically a source of code complexity (which was often trying to address the inconsistency) and bugs. We could achieve consistent behavior with careful specs, synchronizing code across Android and iOS, and rigorous testing, but guaranteeing consistent behavior is much easier with a single consistent implementation. We saw value in ensuring complex client-side logic behaves exactly the same on both mobile platforms. We chose C++ for a consistent implementation because it is the most battle tested cross-platform language and we already possessed the tooling and acumen for mobile C++ development (other areas of Snapchat such as AR Lenses used C++ extensively).
In order to build a consistent implementation and achieve consistent behavior, we needed to answer the question, “What gets built in C++ and what gets built per-platform?” We landed on a simple core tenet for the new codebase: Messaging logic is always expressed in C++ code and the presentation layer lives in Android and iOS code. We used C++ to write consistent logic and defined shared data models for transferring data between C++ and the three critical layers it interacts with: the presentation layer, the network layer, and the storage layer.
The core C++ library exposes an interface that accepts user interactions as input. It also listens for changes to user conversations over the network and translates wire models between C++ and the network layer. As conversations progress, the library publishes changes to message data to the presentation layer and translates persistence models between C++ and the storage layer.
While C++ makes it easy to write business logic once, C++ alone doesn’t make it any easier to write cross-platform glue code or interact with lower layers. In order to bring consistency to the other layers of our stack, we needed to build cross-platform interfaces and consistent implementations for networking and storage.
Consistent interfaces with Djinni
Consistent interfaces allow engineers to establish strong behavioral contracts regardless of language and platform. If interfaces between platform-specific code and C++ code diverge across platforms, behavior will inevitably diverge as each platform follows different patterns for interacting with a component.
Rather than designing our own interface layer, we adopted, enhanced and ultimately took ownership of Djinni, an open source library for generating cross-platform interfaces into C++ libraries. Djinni allows you to design an interface once through its interface definition language (IDL) and then generate consistent Java, Objective-C, and C++ code that exposes that interface and transfers data back and forth. The Djinni interface provides an easy way for Android and iOS developers to integrate messaging UI code with business logic at lower layers.
Djinni was mature enough to use out-of-the-box and get our engineering team started. Over time, we made several performance and memory utilization improvements. You can read more about Djinni and the details of our improvements in this blog post.
Consistent implementation at lower layers
As we went deeper in our rewrite of the messaging stack, we realized we also wanted to have a consistent implementation for making network requests. The rewrite imposed two constraints on the network stack. First, it must support gRPC, as our new microservices architecture emphasizes gRPC. Second, behavior and performance should not differ across mobile platforms. We built a cross-platform client network layer using the C++ client for gRPC, Cronet, and QUIC. We already used Cronet+QUIC extensively in Snapchat because Cronet provides a convenient cross-platform implementation (you can read more in this post about QUIC at Snapchat). The core gRPC client also supports pluggable transports (Cronet is one of the options). All that was left was to bridge the gap between gRPC’s core and its C++ client to enable Cronet+QUIC.
With a cross-platform gRPC client in hand we were able to avoid divergence at the lowest layers of the implementation. We were also able to meet our performance goals and publish comparable client-side metrics on both mobile platforms. In fact, our C++ gRPC client ultimately evolved and now services all gRPC calls in Snapchat from both Android and iOS code.
Consistent data in structured storage
Consistent data is a natural extension of a consistent implementation, and structured storage is a necessity for storing message history and coordinating client/server state. If we already had the same code and the same interfaces, we also wanted our data storage and schema to be the same on both platforms. This was one of the easier problems to solve thanks to SQLite, a popular cross-platform client SQL database that is easy to use on both Android and iOS.
We leveraged SQLite and in-house C++ support libraries to bring consistent data to the table. Since data access is restricted to our shared C++ library, schema and queries only need to be defined once, resulting in identical schema and data on both mobile platforms. Our libraries make working with SQL simple, fast, and flexible, ensuring we raise the bar on performance in our storage layer as well.
What did we get out of this?
Investing in consistency at multiple layers allows us to support the evolving needs of Snapchat with a small team of C++ engineers who own core messaging logic, network transport, and client-side message data storage. Our engineering team is able to build and maintain the majority of messaging code in a single code base through a single set of interfaces, data models, and service APIs. We can develop new features or change how messaging works without worrying about the experience being different on different phones–the implementation and behavior are the same on both mobile platforms. We can now build new features and exciting new messaging experiences at a significantly faster pace than ever before.
Since our initial decision to adopt C++, it has grown into an integral part of Snapchat messaging. By investing in consistent behavior, interfaces, implementation, and data, we delivered a cross-platform messaging product with a lower cost of maintenance that allows us to continue to innovate on the unique Snapchat messaging experience.