This article is the third in a series covering how Uber’s mobile engineering team developed the newest version of our driver app, codenamed Carbon, a core component of our ridesharing business. Among other new features, the app lets our population of over three million driver-partners find fares, get directions, and track their earnings. We began designing the new app in conjunction with feedback from our driver-partners in 2017, and began rolling it out for production in September 2018.
The competition between urban architecture and wireless data technology means lapses in coverage—dark spots in cities where our phones won’t work. Driving through urban landscapes means finding more of these dark spots, leading to frequent changes in network quality and levels of congestion. These lapses in coverage particularly affect Uber’s driver-partners as they attempt to pick up or drop off riders.
The pain points here can be demonstrated best by an example. Suppose a driver finishes a trip at a crowded airport in Bangalore. The rider wants to pay with cash, and the driver needs to complete the trip in the app to see the final fare. Pulling up to the curb at the airport, the driver’s phone can’t connect to the Internet. The rider is rushed to make their flight, but the lack of a connection means the driver can’t complete the trip in the app and get the final cost. The driver might drive further down the terminal, taking extra time, potentially extending the trip, and causing frustration for both rider and driver.
To deal with lapses in network coverage and prevent these types of scenarios from occurring, we came up with Optimistic Mode. This new feature for our driver app lets the app work offline so that a driver can end a trip even without a connection and retrieve the last price estimate received from the server. Optimistic Mode allows the app to work regardless of network conditions, leading to more positive trip experiences for rider and driver alike.
Optimistic Mode components
We supported some offline capability in the previous driver app by collecting failed requests and batching them to the server to be consolidated once connectivity was regained. While this feature helped prevent some errors from being displayed, it wasn’t able to intelligently update the state of the application, stack multiple actions on top of each other, and persist state across sessions. We developed the components described below for our new driver app to deal with these issues.
Any component of the driver app capable of operating optimistically begins the flow by submitting an optimistic request. An optimistic request has the ability to serialize and deserialize to disk, very similar to a regular network request, and every optimistic request is paired with an optimistic transform.
The main component that allows Optimistic Mode to work are called transforms, in other words, operations that transform the current state of an object to an optimistic state, i.e., the expected state to be returned by the network. Transforms can also be stacked, applying their changes in order as an object passes through each transform. To understand transforms with a simple example, let’s imagine a class “Counter” which has a property “count.” We can then implement a transform which increments the count property of the Counter object.
Transforms can be as simple or complex as needed for our optimistic operations. Each optimistic request has a transform associated with it. The transform outputs an optimistic state that matches the eventual response from the optimistic request. This way, the user will not notice any change in the app when the response comes back from the network, providing a smooth transition.
When an optimistic request is submitted to the client, the transform associated with the request is applied immediately to move the app into an optimistic state, making it appear that the request has completed. The optimistic state outputted from the transform will be maintained until a response from the server is received with the actual state, syncing app and server.
We use RX streams as the message bus for data to be passed through the app. Every feature in the app reacts to the state changes that are published on the datastream. This mechanism enables us to use the same stream to easily apply optimistic transforms to the latest state of the object. To obtain the optimistic state, we combine the last known state of the data on the stream with the available transforms for the data. The data has each transform applied to it before being published back on the stream and consumed by the feature. The feature then simply reacts to the optimistic state of the data.
There are also requests that are dependent on optimistic requests completing. For example, it wouldn’t make sense to send a request to end a trip that the backend doesn’t even know has started. Such dependent requests will be queued for a period of time while we wait for the optimistic requests to complete. If this period is too long, we fail the request, notifying the user with a network error message.
We faced several challenges in this design. We wanted to support stacking optimistic requests, allowing for multiple steps to be completed without a network connection. Due to being out of sync with the server we also needed to handle cases where we incorrectly moved into an optimistic state and must revert to a previous state. Ensuring that we show the driver the most accurate state reliably is something that took several iterations and will continue to be optimized as we move forward.
With Optimistic Mode enabled, the application may receive other network data before the optimistic request has been able to complete.
For example, let’s take the counter example we used above. The app increments the count using the transforms to give a final value of 2. However, this count has not yet synced with the server. During this period of time, other network responses received may have a stale value of 1. Optimistic Mode uses the transforms to update the stale state and maintain the optimistic state. This ensures that the app does not revert back and forth for the user, between two states, avoiding a confusing experience.
Surviving application restarts
All optimistic requests along with the last known optimistic state are saved to disk so they persist across application restarts. Consider a scenario where there are a few requests queued up to be synced with the server, but the user closes the app. Upon re-launch the optimistic requests and last known optimistic state are loaded from persistence. This allows the users to be in the same state when they re-launch the app. The optimistic requests are queued up to sync with the server.
A particular issue we come across with this new feature is how it surfaces errors. Optimistic Mode was designed for requests that should only fail due to back-end outages, and should have predictable responses that can be mocked. However, in practice errors will arise. Because we move the user through the app workflows optimistically, an error can be a very jarring experience. Firstly, the app state rolls back to the pre-optimistic state, leaving the user in an unexpected state where the next action may not be obvious. Secondly, in order to surface errors we need the previous state to receive error messages, even though it may have already gone out of scope. To handle this, in the driver app we create a global error handling framework, which we call internally the Alert Framework.
There will always be the rare case when a server returns an error to a request. For commonly occurring error, such as when trips are too short, we implemented checks on the mobile clients to handle them better.
For drivers, we’ve seen great savings in time spent starting and ending trips, which are the first two operations that utilize Optimistic Mode. We often see that a trip was able to progress several minutes before a network operation was actually able to complete. As of November 2018, we have observed that the average time saved per optimistic operation is about 13.5 seconds. Even at this early stage in the new driver app’s life we are totaling over a year’s worth of continuous driver time saved in aggregate each and every day.
The future of Optimistic Mode
The ability to progress user state without a network connection has shown to be useful for other flows in Ubers apps as well. Launched as a way to speed up starting and ending trips, it has also been integrated into delivery-partner features for Uber Eats, allowing for quicker drop-offs when cash is used as payment. It can also be leveraged for features that need to react quickly but can sync with the server later, such as rating a rider or driver, marking inbox messages as read, or collecting signatures for deliveries.
Index of articles in Uber driver app series
- Why We Decided to Rewrite Uber’s Driver App
- Architecting Uber’s New Driver App in RIBs
- How Uber’s New Driver App Overcomes Network Lag
- Scaling Cash Payments in Uber Eats
- How to Ship an App Rewrite Without Risking Your Entire Business
- Building a Scalable and Reliable Map Interface for Drivers
- Engineering Uber Beacon: Matching Riders and Drivers in 24-bit RGB Colors
- Architecting a Safe, Scalable, and Server-Driven Platform for Driver Preferences
- Building a Real-time Earnings Tracker into Uber’s New Driver App
- Activity/Service as a Dependency: Rethinking Android Architecture in Uber’s New Driver App