January 2018 marks the two-year anniversary of the Uber Eats app on Android. With this in mind, software engineer Hilary Karls reflects on her team’s experience launching this app in 2016 and discusses how their commitment to “building a codebase you can live in” resulted in one of Uber’s most successful products.
Shipping an app probably is not the first thing that comes to mind when you think of sports. When you boil it down, however, a successful launch is shaped by the same forces that contribute to a perfect game: the ultimate alignment of vision, determination, drive, and teamwork.
In many ways, the Uber Eats Android app was our team’s perfect game.
In August 2015, we were wrapping up building a new home for Instant Uber Eats (our first iteration of the Uber Eats platform) in the Uber rider app. Specifically, we were testing food delivery by adding a toggle in the app’s header that enabled riders to switch from requesting a ride to viewing restaurants they could order from. Through these tests, we found that users preferred greater variety—full menus from a much wider array of restaurants—over immediate delivery. While they wanted their food fast, a 10-minute delivery time was not as magical with limited options.
To create a product that would better satisfy the preferences of hungry Android users, we decided to build a new app—Uber Eats. The initial challenge: spend six weeks building a viable version of an app that would be easy-to-navigate, useful, and, most importantly, make the food delivery experience as convenient and enjoyable as possible.
Ambitious? Sure, but it set the tone. As our team’s designer put it, the first iteration of Uber Eats for Android was “the cupcake version”—functional and scaled down, but still delightful.
The warm up: building the foundation
The first draft of our designs were ready by the end of August. By early September, the broader engineering team had assembled a plan and a detailed outline of what needed to exist to power our app on both iOS and Android.
Over the remainder of the month, we rolled off of other tasks and started planning and executing in earnest. After looking at the project’s scope, those of us who had been in the industry for awhile knew this would take complete focus and dedication through the end of December.
In addition to shipping quickly, we were determined to develop for future growth. We borrowed liberally from best practices elsewhere in Uber Engineering, developing on top of internal and external libraries. We used the MVC framework from our driver app and integrated RxJava in the same way we had for our rider app.
To build a solid foundation from which to work off of, we pored over resources on these technologies; this app was code we would be living in for a long time. We leveraged the Uber Android Platform team’s networking framework, which was built on top of OkHttp, Retrofit, Dagger, and our own experimentation framework. We challenged each other during code reviews, gauging if our foundational code was simple or well-documented enough that our future selves would be able to understand it.
Working with product, design, and other teams, we developed user stories to better understand what functionalities we needed to build. These specifications ranged from considerations as basic as “a user should be able to login” to “a user should be able to place an order” with a restaurant. With these guidelines in mind, we worked with our back-end and iOS teams to define the Uber Eats APIs as clearly as possible.
Even though we knew our APIs would adjust, we began implementing, having a shared understanding among teams enabled us to decouple our execution. Additionally, agreeing on how we model objects in our system made it way easier for us to build sustainable and uniform versions of the app across both mobile operating systems. Aligned on our plans and strategy, we started building the happy path.
The first half: the happy path in theory
In mid-October, we hit our stride after one of our team members suggested that we divide the work into two-week checkpoints, a practice we adopted readily. We assigned owners for each group of features, and started bucketing them, aggressively slotting the riskiest and most critical work first.
For the app to work perfectly, we needed our happy path to check off several boxes. First, it needed to be easy for users to sign up and add their payment information. Next, a user had to be able to input their address entry. Then, once a person entered their delivery location, the app needed to surface all the restaurants in a particular area and allow users to view their menus. Once there, users needed to be able to view dish pages and add dishes to their carts. And finally, it needed to be easy to checkout and and track orders from the moment users pressed the “Order” button through delivery. To ensure that our happy path was just as ‘happy’ in theory as it was in practice, we all pitched in to code review, test new functionalities, and fix bugs.
We ran hard everyday, trusted each other to do good work, gave constructive feedback, and took responsibility for our respective contributions. Sometime in late November, I was sitting in a team meeting looking around at everyone. An image of a soccer field, players running and passing, cheering for each other—playing the perfect game—popped into my head, and I realized how we seamlessly flowed together. And even though the game got harder the farther we got, our patience frayed, emotions ran high, and none of us were at our best all of the time, we trusted each other to get the app across the finish line.
The second half: the happy path in the real world
By early December, the iOS and back-end teams were facing our greatest integration hell. It was time to see how ‘shared’ our shared understanding was and what would actually work in practice. As it often does, this process took some iteration. After resolving lingering issues, the two teams flew to Toronto to launch Uber Eats iOS on December 9, 2015.
Meanwhile, our Android team was wrapping up the last features on our version of the app. At the same time, we had to keep an eye out for any changes that came out of the iOS and back-end integration. Shortly thereafter, we were able to place our first orders through the app, and actually get food delivered (though we had not yet built delivery tracking, so waiting for our order was a little more stressful than it is now). My favorite item to order was always the chocolate chip cookies from Mr. Holmes Bakehouse—I could actually eat my test cookies!
Looking at data around our iOS launch in Toronto, we could see that some of our simplifying assumptions produced less than optimal user experiences. For example, we had hoped that all of the restaurants would go online basically at the same time. Instead, it was a few here and there, then a lot, and then a lot more, and given that our list ordered open restaurants above closed restaurants, the list of available options shifted tremendously every few seconds. It felt like the app equivalent of an earthquake, and we knew we had to fix it before the Android launch.
To address this, we added code to detect changes in the number of open and closed restaurants, and refresh inline if there were only a few changes, prompting the user before applying refreshes if it was over a certain threshold.
We also realized that there were too many restaurants to navigate in a single list without incorporating a search functionality. So we built the first, client-side version of search in 48 hours, even incorporating spell checking and suggestions using Levenshtein distance calculations.
A fine finish
We were still running hard up to our code freeze in late December. While the iOS and back-end teams were out celebrating the success of their launch, we stayed back, ordered pizza, and fixed lingering issues, code reviewed, and landed every remaining bug. Energized by our camaraderie, we reached the finish line radiating pride and excitement, amplified that much more by exhaustion. We laid it all on the field, walking away from our work with hugs and high fives.
Since it was a bit risky to ship an app over the holidays (while most engineers were concerned with supporting New Year’s Eve traffic), we decided to fly out to Toronto after the New Year for our launch.
Over the holidays, I took advantage of the opportunity to lead a workation project in Roatan, Honduras. I knew that I would be happy sunbathing, scuba diving, and implementing TalkBack accessibility in the app. So I ended up working with my colleague Erik on the beach in Honduras with Jesus supporting our code reviews from the Bay Area. So, we said goodbye to 2015 by making Uber Eats more accessible for Android users, swimming with sharks on New Year’s Eve, and becoming supremely sunburnt, just in time for our launch on January 6, 2016 in chilly Toronto.
Two years and 200 cities later
Two years later, the Uber Eats Android app has expanded to over 200 cities worldwide, with over 80,000 restaurant partners. Our hard work paid off: in 2016, the app won an Editor’s Choice Award from the Google Play Store and has been downloaded by millions of users worldwide. While we have slowly re-written parts of our app as we have launched new features, some of our original code is still in use today.
While Jesus and I are still on the Uber Eats team, Paulina and Padmini have taken their entrepreneurial spirit to Uber Freight. Playing the perfect game with them taught me several lessons about what it takes to successfully build and launch a new app:
Partner with your operations teams to deliver growth
None of the growth we saw with Uber Eats would have been possible without our operations teams. Their early testing in Los Angeles and the handful of markets that experimented with Instant Uber Eats proved that the business would stick so that we could confidently expand. Additionally, the Toronto team worked closely with us and their local partners to launch the first version of the full app and marketplace and poured their energy into making it work.
Practice: experiment early and often
We had several experiments before we committed to building a separate app. The insight that we gained by conducting relatively low investment experiments early on gave us the confidence to build a new app, and to know that we would find a path to something useful.
Building on top of frameworks and libraries from Uber, Square, Google, and the broader engineering community saved us a lot of time and effort. Do not reinvent the wheel unless you have to start from scratch.
Prioritize the key/risky features first
It is good practice to break down features into “must have” (P0), “important” (P1), “nice-to-have” (P2), etc. categories. For this rollout, we were almost exclusively focused on the P0s. And even among the P0s, it is important to have a sense of which features are the riskiest—the most likely to take more time than expected. If those can be knocked down to P1, do it. If not, slot them earlier in the development schedule. Should timelines shift, the flexibility will make it easier to ship, either by changing the scope of the rest of the project or by agreeing on a new launch date with your stakeholders.
Leave time for integration
Even with the best planning, the final moments of any project may reveal inconsistencies in understanding or assumptions that were not quite accurate. Budgeting a bit of time for integration, testing, and bug fixes gives you a chance to address any issues with less stress.
Run in the same direction: alignment and values matter
Although this is widely known, alignment on what needs to be done is essential to execution. Confusion around what is being built, by whom, and when wastes valuable time. Along those lines, it is also important to have alignment about how you and your team choose to build. We were all aligned on wanting to build an app that we could live in for at least a year without having to rewrite as soon as we launched. If some members deeply valued code quality and some valued shipping as quickly as possible and cleaning up everything later, the inevitable tension would have slowed us down tremendously.
Take care of each other
Our team worked well together because we trusted each other both to execute and help each out if anyone got stuck. The broader Uber Eats engineering team, too, values this same sense of trust and support. We know that if we need help with anything—from code reviews to fixing bugs to collaborating on features—we have each others’ backs, empowering us to produce something far greater than we can alone.
If serving up something delicious on Android interests you, consider applying for a role on our team!