📜 ⬆️ ⬇️

Developing a new application architecture for Uber passengers

- Hello. Tell me, how much does an Uber app cost?

The incoming call manager of our company receives calls with such content consistently once a week. Understanding it is usually the case: either the client wants just as successful an analogue of the application for communication between the passenger and the driver, or Uber for ______ (enter the desired industry).

At such moments, we answer that Uber is a technically very difficult project with millions of investments and hundreds of thousands of man hours of development, and that making it a clone is not very useful.
')
Now we have an argument in defense of our position. Uber developers have published a note in the company's blog about the experience of transferring applications from one architecture to a new, own one. This very large-scale event confirms that Uber is far from an elementary application. We could not pass by this material and not translate it.

The article can be useful not only for mobile developers, but also for managers facing the described situation.



Why we remade Uber


The idea of ​​Uber is simple: click on the button and you will be taken where you want. Having started as a service for ordering premium black cars, today Uber provides a huge range of services, coordinating millions of trips in hundreds of cities every day. To comply with the realities of 2017, we had to re-develop the entire application architecture.

But where to start? From the same thing we started in 2009: from scratch. We decided to completely rewrite our application and redo its design. The refusal of the accumulated software base and design decisions gave us freedom of action where otherwise we would have to look for compromises. At the exit, we got a completely new application, licked to shine and giving all the advantages of its new architecture to both iOS users and Android users. This article describes how we created a new architectural pattern called Riblets and how we achieved our goals with it.



Motivation and goals


While the main idea of ​​Uber is still the provision of communication between drivers and passengers, our product has grown into something much larger, and the architecture of the former application was no longer able to cope with the increased requirements. Adding new features to the application from year to year became harder. Additions such as UberPOOL, scheduled trips and car photos only made it harder to work. The risk that some part of the application would stop working after making the next change increased day by day along with its size, due to which any experiment could be the cause of a long debugging.

At some point, further growth was simply impossible. In order to maintain the high quality of customer service, we needed to re-examine the simplicity with which we once started, given current requests and the ability to easily evolve in the future.

The new application should be simple both for passengers and for Uber developers, who are working every day to improve it and add new features. To remake the application taking into account the interests of both the first and second, we set ourselves two tasks: the result should be as accessible as possible to passengers and allow radical experiments to be carried out within the product.

Reliability is what counts.


The task of the developers is to create an application, the reliability of which will be 99.99%. This means that the application can be out of service for no more than one hour per year or 1 minute per week, and for 10,000 starts it is given no more than one failure.

To achieve this, the structure of the new application was divided into primary and optional codes. The main code - everything related to the entrance to the application, confirmation, completion and cancellation of the trip - should work like a clock. Any changes made to the main code are rigorously tested. Changes made to the optional code are less rigorous and can be disabled without the need to stop the entire application. Thanks to this code isolation, we have the opportunity to add new features and in the case of incorrect work automatically disable them without delivering any inconvenience to the user.

Future plans


We need a platform for which hundreds of teams and thousands of engineers will be able to develop high-quality add-ons and embed them in the application, without causing any inconvenience to passengers. Therefore, we have made our new architecture cross-platform, so that Android and iOS developers can work on an equal footing with it.

Usually, in order to create the perfect application for Android and iOS, a separate approach is required to its architecture, libraries and analytics tools. The new architecture should use the same patterns and practices for both platforms. This will allow us to learn from our own mistakes with the maximum benefit, applying the solutions found on one platform, while working with another. As a result, Android and iOS developers can work closely together to create new options and add-ons.

Although in some cases, development takes place individually for each platform (for example, the implementation of the user interface), both platforms have a lot in common:


To make the most of these common features, our new architecture requires a clear organization and separation between business logic, presentation logic, data flows and routing. This architecture helps to avoid intricacies, simplify testing and, consequently, increase the productivity of development and reliability of the final product.

From MVC to Riblets


Having defined the tasks, we decided to find out how to improve our old architecture and started exploring the possibilities. The codebase we inherited from the old version of Uber was based on the MVC architectural pattern. We studied other patterns, for example, VIPER, which we partially used to create Riblets. The main innovation of Riblets is routing through business logic instead of presentation logic. If you are not familiar with MVC and Riblets, read a few articles on modern architectural patterns for iOS (for example, this one ). This will make it easier for you to understand the advantages and disadvantages of adapting these patterns to Uber.

What we started with: MVC (Model-View-Controller)


The first application for passengers was written by a small group of developers almost four years ago. At that moment, using MVC seemed justified. When the development team grew to a few hundred people, we were confronted with the fact that MVC cannot grow with us. There were two main reasons:

  1. The growing MVC architecture often faces a problem called the massive view controller. For example, RequestViewController, which begins with 300 lines of code, contains more than 3000 lines in its current state due to the large number of responsibilities assigned to it: business logic, data manipulation, data validation, network logic, routing logic, etc. It became very difficult to read and modify it;
  2. MVC architecture provides a very fragile code update process and complicates testing. In the process of developing new additions, we are experimenting a lot. All our experiments are reduced to working with if-else statements. Every time a class with a lot of functionality comes across, the if-else statements lean on each other, reducing the possibility of testing. In addition, with the growth of internal pieces of code, such as RequestViewController and TripViewController, creating updates for an application has become a very fragile and sensitive process. Imagine what it is - for every possible change to test all possible combinations of if-else statements nested into each other.

Since we wanted to continue to experiment with the goal of further developing the application and growing Uber's business, we had to admit that this architecture has exhausted itself.

On the way: VIPER


In the process of searching for an alternative to MVC, we found VIPER, which is an example of applying pure architecture in developing iOS applications. VIPER has several key advantages over MVC:

  1. he offers much more abstraction. Presenter contains logic that connects business logic with presentation logic. Interactor handles data manipulation and verification. This includes requests to the backend to manipulate the state, for example, to enter or book a trip. And finally, Router initializes transitions (one of them is like a transition from the home page to the order confirmation page);
  2. in the case of VIPER Presenter and Interactor are Plain Old Objects, so we can use regular unit testing.

But we also found a few shortcomings with VIPER:
  1. its design, specialized for iOS, meant that we would have to look for compromises for Android;
  2. its view-driven logic means that applications are controlled by the components of the view, and the entire application is tied to the view tree;
  3. The business logic executed by an Interactor, which must control the state of the application, must always pass through the Presenter, in which it is lost;
  4. with view trees and logic closely related to each other, the implementation of an element that contains only one type of logic becomes very complex.

Being clearly better than MVC, VIPER cannot fully satisfy Uber’s need for an extensible platform with clear modularity. Therefore, we returned to the drawing board to try to develop an architectural pattern with the advantages of VIPER and without its drawbacks. The result was Riblets.

Riblets: Uber Passenger Application Architecture


In our new architectural pattern, logic is broken into small independently tested pieces. Each of the pieces has a single purpose in accordance with the principle of sole responsibility. We use Riblets in the form of these modular pieces, and the structure of the entire application is the Riblets tree.

Riblets and their components


With Riblets, we have divided responsibilities into six different components in order to abstract business logic from presentation logic even more:



What makes Riblets different from VIPER and MVC? The route is laid by business logic instead of presentation logic. This means that the application is controlled by the flow of information and decisions made, and not by appearance. Not every piece of business logic in Uber is related to something that the user sees. Instead of using business logic in ViewController in MVC or manipulating application states via Presenter in VIPER, we can make a separate Riblet for each piece of business logic, creating local groups that are much easier to work with. In addition, we developed the Riblet pattern in such a way that it does not depend on the platform. The latter allows you to combine development for iOS and Android.

Each Riblet includes R outer, Instructor and B uilder with its own Component (hence the name) and, if necessary, Presenters and Views. Router and Interactor are engaged in business logic, while Presenter and View are engaged in presentation logic.

Let's look at what each element of Riblet does, using Production Select Rible as an example.


Tariff selection in the new Uber application

Builder
Builder installs all the primary elements of Riblet and the dependencies between them. In Product Selection Riblet, this element sets the dependency of the data flow for the desired city.

Component
Component gets and installs Riblet dependencies. This includes services, data streams, and everything else that is not the primary element of Riblet. Product Selection Component receives and establishes a dependency on the city flow, attaches it to the corresponding network events and injects it into Interactor.

Routers
Routers form the application tree by attaching and undoing child Riblets. These solutions are passed to them by the Interactor. Routers also control the life cycle of Interactor, turning it on and off in certain application states.

Routers contain two pieces of business logic:

  1. Helper methods - to connect and disconnect Routers.
  2. State-switching is the logic for determining the state of the child modules.

Product Selection Riblet has no child Riblets. Router of its parent Riblet, Confirmation Riblet, is responsible for attaching the Product Selection Router and adding its View to the Views hierarchy. Then, when a product is selected, Product Selection Router deactivates its Interactor.

Interactors
Interactors perform business logic. This includes:



Product Selection Interactor takes a city stream containing data, including offers from the services of this city, pricing information, approximate travel times and photographs of cars, and transmits this information to Presenter. If the user moves from uberPOOL to uberX, Interactor receives this information from Presenter, after which he collects all the relevant data and sends it back to View so that it displays the uberX cars and estimated time of arrival. In short, Interactor executes all the business logic that the View then displays.

View (Controller)
Views configure and update the user interface, including the creation and location of individual elements, user interaction, and the filling of interface elements with data and animation. View in Product Selection Riblet displays the objects that Presenter passes to it (trip configuration, prices, estimated time of arrival, car image on the map) and returns user actions (i.e., product selection).

Presenter
Presenter manages communication between Interactors and Views. From Interactors to Views, the Presenter delivers the business models of the objects that the View displays. In the case of Product Selection, the Riblet is pricing and vehicle image data. Also, the task of Presenter is to convert user actions (such as clicking on the product select button) into commands, which are then passed to Interactor.

Putting it all together


Each Riblet contains only one pair of Router and Interactor, but it can have several parts of the presentation. Riblet, which is solely responsible for business logic and has no user interface elements, does not contain a view part.

Riblet can be:


This allows business logic trees to be deep and different from flatter view trees, making switching between screens easier.

For example, Ride Riblet is viewless. Its task is to check whether the user has an active trip. If so, it connects Trip Riblet, which shows the trip route on the map. If not, the Request Riblet is connected, showing a screen on which the user can book a trip. The main task of Riblets that do not contain presentation logic (such as Ride Riblet) is to isolate the business logic that controls our applications, thereby preserving the modular structure of our new architecture.

How to create an application from Riblets


Riblets are combined into an application tree and they need to keep in touch with each other in order to update information or guide the user to the next step of the trip order. Before we tell you how they communicate with each other, let's see how the data is transmitted within a single Riblet.

Data stream inside Riblet


Interactor stores the business logic that controls the application and is within the purview of this Interactor. This element makes service requests to get the necessary data.

Data in the new architecture is always transmitted in one direction only. They move from Service to Model Stream, and then to Interactor. Interactors, event planners, and push notifications from the Internet can query Services to make changes to the Model Stream.

Model Stream creates immobile models. This creates requirements according to which in order to change the state of an application, the Interactor classes must use the service layer.



Examples of threads:



Interaction between Riblets





When Interactor makes a decision, he may need to inform the other Riblet about it and send the data. For this, Interactor invokes an interface that negotiates it with the Interactor of another Riblet.

In case the connection goes up the tree to the Interactor of the parent Riblet, the interface is defined as a Listener. The listener is almost always set by the Interactor of the parent Riblet. If the connection goes down to the child Riblet, the interface must be defined as a delegate, and the Interactor of the child Riblet is set. Delegates are used only for direct synchronous communication between elements, for example, a parent Interactor with a child.

In the case of a downlink, the parent Riblet can direct the data stream to the Interactor child of the Riblet observable, the parent Interactor can then send data through this stream instead of the delegate interface. In most cases of downlink communication, a similar method should be preferred.

For example, when a hypothetical ProductSelectionInteractor understands that a product has been selected, it refers to its listener established by the ConfirmationInteractor and passes it the view identifier (ID) of the selected vehicle. The ConfirmationInteractor saves this ID in order to send it in the request to the services and sends to its Router a request to “disable” the ProductSelection Riblet.

By structuring in a similar way the flow of data within and between Riblets, we can be sure that the right data will be displayed at the right moment on the right screen. Since the Riblets tree is based on business logic, we can communicate at the level of business logic instead of presentation logic. This makes a lot more sense and helps to keep code isolation, protecting application development from unnecessary complications.

Back to the roots


When we decided to completely redo our application, we wanted to get closer to the user due to the increased reliability and create the right conditions for future developments. To achieve these two goals, the creation of a new architecture was a vital step.

How we have achieved increased reliability and availability for the end user


Riblets have clearly defined responsibilities, so testing has become much easier. Each Riblet can be tested independently. With such features, we can be sure that our application will not get bugs after the next update. Due to the fact that each Riblet bears one and only responsibility, we were able to easily separate Riblets into the main one (necessary for entering the application and ordering a trip to uberPOOL and uberX) and an optional code. By making higher demands on checks of the main code, we can be sure that the main functions of the program will work with maximum reliability.

We also made possible the global rollback of the main functionalities of the program to a guaranteed working state. All optional code is marked with flags master feature, which can be turned off if parts of the code contain errors or are unstable. In the worst case scenario, we can disable absolutely all optional code, leaving only the base. Given our stringent requirements for the main code, we can be sure that it will always work.

How we created the right conditions for future development


Thanks to the narrow specialization of each Riblet, we were able to draw a clear line between business logic and presentation logic. This can prevent the inordinate growth of the code base and preserve its simplicity. Since the new architecture is cross-platform, iOS and Android developers can easily understand each other's work, learn from the mistakes of their comrades, and work together to further develop Uber. Experiments will have less influence on the performance of the application core, since Riblets helps us to separate the optional code from the main one. We will be able to test new add-ons created in the form of plug-ins, without fear that they may accidentally disable the entire application.

Since the main feature of Riblets is the maximum abstraction and separation of responsibilities, as well as well-defined data streams and methods of communication, further development of the project becomes very simple, and this architecture will serve us faithfully for many years.

Willingness to go ahead


We have high hopes for our new architecture. We completely rewrote the application code for passengers, re-adding everything that already existed previously, conducting user surveys, case studies, various tests and updating the options, among which there is now a news line. We tried to do everything possible so that our users received the updated application as soon as possible, so we listen to feedback from around the globe regarding design, options, localizations, work on various devices and testing capabilities. Despite the fact that the launch of the application is already behind, there is still a lot of work ahead.

Thanks to the new architecture, we have a huge number of opportunities for further development. To be honest, we spent a couple of months building prototypes to make sure that we did everything right. Now we are fully confident that we have an architecture with which we can achieve a lot. If you like this job, you can become part of our history and improve your view on Uber by participating in development as an iOS or Android developer.

Source: https://habr.com/ru/post/320452/


All Articles