A few years ago, I was leading the design and development of a global ride-hailing app used by millions.


It was a fascinating and memorable experience. Only with time did I truly realize the scale of the changes I brought into the app and the new paths for expansion and scaling that it opened.


Despite the fact that I was doing this inside a large ride-hailing company, we worked there like in a real startup. We were building solutions for 360 million users literally on enthusiasm and on the desire to create a new, modern, breakthrough product.


I’ll share the bumps I hit while building the new order form with the ability to choose the type of transport you need. How I implemented it and how everything ended.


What the problem was

Before this feature was implemented, users in the app could order only one type of ride — a regular car.


This limitation significantly complicated the use of the app, because very often the car that arrived simply didn’t fit the situation. For example, a user wanted to travel quickly and comfortably from point A to point B, but instead an old car arrived that could barely move.


This forced users to write their requests and preferences in the comments. Another important limitation was that drivers with expensive cars simply refused to work in the app.


The task in one sentence: we needed to give the user the ability to choose the ride type (economy, comfort, premium, auto rickshaw).


What we wanted to achieve

The most important thing I would advise when developing any feature, no matter how large or serious it is — before you start coding, implementing, or even drawing any diagrams — you must make sure you understand the problem.


How to check yourself? Try to explain what you are doing and why to any person who is not involved in the topic. If you cannot explain in 3–4 sentences what is needed and how to achieve it — then you need at least one more iteration of formalizing the requirements and researching the problem.


What we wanted to achieve:


Advice: study the problem, understand what and why needs to be done — before you even start thinking about the implementation.


How we approached the task

It was decided to completely redesign the order form. The old form had become so outdated that it already stood out from the rest of the app.


The old form had survived almost entirely in the same shape in which it was created back in 2013, and now it was already 2020.


I must admit, the inspiration came, of course, partly from other top ride-hailing apps like Uber, Lyft, and Gett, where ride types had already existed for quite a long time.


As you can see in the image above, we introduced the ability to choose one of the N ride types.


Meaning: if a user is ready to spend more on the trip and ride in comfort, they will get a newer car, but still a mid-class one. And if they don’t need to go far, they can, for example, call an auto rickshaw — a super popular type of transport in India and the Asian region — and get a very cheap ride.


Code and architecture

I won’t go too deep into the weeds of architectural decisions, APIs, and everything else — in general, I can show everything in one picture:


Of course, later on we moved to a microservices architecture, but during the development of the new order form, in order to meet the deadlines and avoid a massive refactoring, a reasonable decision was made — to implement everything directly inside the monolith.


The difficulties we faced:


The advantages we got from working inside the monolith:


Advice: when starting development, don’t rush to rewrite everything, change everything, or create new services. First, make a naive implementation, write the code inside the existing codebase, and only then move it.


Managing the new order form

The app operates in 40 countries, and each country has its own specifics. For example, in the Asian region, moto transport is popular, while in Europe moto-taxis would be considered something strange.


Therefore, it was very important to give regional managers the ability to configure the new form on their own.


They could specify:


It was important to provide a simple and understandable tool so that operational staff on the ground could configure everything individually for each country.


And, as I mentioned earlier, enabling the feature country by country allowed us to turn it on in small batches, one country at a time, and avoid unexpected problems.


Advice: if you are building an admin panel to manage some feature, it doesn’t need to be beautiful or extremely convenient — it needs to be simple. It should contain only the minimum required, but still be very clear and easy to understand.


Vehicle classification

So, the tool for configuring the new order form with ride-type selection was ready.


The most important part remained — the heart of the new form — we needed a way to distinguish which car, motorcycle, or any other vehicle belonged to which category.


And this was actually a very non-trivial task, because across 40 countries around the world, each region has its own unique types of transport. There are hundreds of different Mercedes modifications alone, across different years.


So we needed some function:


Func(transport_type, brand, model, year) -> order_type


Meaning: based on the parameters the driver specifies during registration, we calculate which ride types they are allowed to perform.

Simplified, the formula visually looked like this:


We took the entire dump of all possible vehicle models and, based on analytics and labeling, built a classifier. 


Accordingly, when a user placed an order of a specific type, the system suggested it to those drivers whose vehicles the system had determined as belonging to that type.


What was the most difficult

During development we encountered several very serious challenges, namely:


All of this, combined with my limited experience in the project and a weak understanding of the domain, became a real challenge for me, and I honestly still don’t fully understand how we managed to solve all these difficulties.


Unexpected development

When we launched the new ride types, a more global task appeared — to build a “super app”, meaning the evolution of the application toward unifying the main flow with the additional ones.


For example, if earlier to order groceries from a supermarket you had to download and register in a separate app, then in such a super app you can do everything in one place.


The new form made it possible to place everything in one place, right on the order panel. For example, cargo taxis and intercity rides were previously available only through the menu, and users simply didn’t even realize that this option existed. Here, we immediately gave the full set of choices.


I’d also note that the form configuration turned out to be flexible — maybe even unintentionally — and it allowed us to set up new ride types even without any specific transport. For example, it was possible to add the option to order flowers in just 5 minutes.


Advice: you should always look wider. In my case, what seemed like a task about separating different transport types actually revealed the possibility of unifying all modes into a single form.


How to break production

Despite the fact that the code was well tested, we still ran into serious problems. The thing is, for various reasons, all tasks and all merge requests ended up in one release.


Considering that the release was in the old legacy system and inside the monolith, this led to the situation where, after an error was discovered, it was impossible to roll back quickly — which resulted in a long-lasting drop in orders.


Advice: beware of large releases, especially in a monolith. The more atomic the release — the better. The bigger the release — the easier it is to miss a bug in it, both during review and during testing. But, within reason, releasing one line at a time is the opposite extreme.


Interesting insights about users

I’d like to share some very interesting observations I made for myself while developing and rolling out the new form and ride types.


1. Functionality is more important than text

As I already mentioned, all ride types are managed from the admin panel, where the manager specifies the name of the new type after creating it and also indicates whether the “details” button is available.


If no description is provided in “details,” a placeholder is shown. That’s exactly what happened. As a result, we worked like that for quite a long time. 


But in reality, users didn’t care, because no one clicked on this info, and those who did saw the placeholder but didn’t pay any attention to it. Because the functionality itself worked, and that was what mattered.


UI polish">

Conclusion: you shouldn’t chase perfect UI/UX — the most important thing is that the functionality performs the needed function.


2. Users are very conservative

As I already mentioned, before we introduced the new order form, users found their own workaround to clarify what exact car they needed, whether they needed a child seat, and so on — they simply wrote all their wishes and requirements in the comments.


For example:


“Car not older than 5 years. Clean interior. No smells.”

“Need a large car, minimum 4 adults + luggage. No small sedans.”

“Child seat required. Driver must drive smoothly and avoid loud music.”


And they continued doing the same even after we introduced the ability to specify this through the ride-type selection and the control checkboxes.


Breaking this habit turned out to be a very non-trivial task, and in some places we even had to disable the ability to leave comments entirely. Meaning, out of habit they would choose “economy” and then write inside the comment: “need a new fast car.”


Conclusion: users are very conservative, and if a flow has worked for a long time (years), it is almost impossible to retrain them — you have to remove the old options through restrictions.


3. Users use functionality in unexpected ways

The new form, for obvious reasons, caused resistance among the drivers who were taking expensive orders while driving cheap and old cars.


This group of users showed a very high level of creativity in trying to return the ability to take premium orders: they cheated by registering expensive cars but actually driving older and cheaper ones.


All of this led to the vehicle verification process being strengthened — if earlier they had to re-upload photos every 30 days, now it was every 10 days.


Conclusion: you should never underestimate the inventiveness of users, especially when it directly affects their earnings — always plan countermeasures in advance.


What lessons I learned

It was a great experience. I came into the company and immediately got the task to “rewrite everything and throw away the 7-year-old order form and build a new one.”


The insights I gained during development:


A big company is still a startup

I love tasks like this, and I dream of one day creating my own company. But in reality, even in a big old company, one that has been on the market for 5 or 10 years, there is still room where you can truly turn around.


Everything turned out great because there were no limitations, no rules, no norms telling us that things should be done this way or that way. We had a clean slate, the task was given at a high level in the spirit of “create,” and we created.


Advice: don’t limit yourself, don’t put boundaries around your thinking, work even in large creaky companies as if you were in a startup — only then can you build truly high-quality and breakthrough things.


How it all ended

Below I’ll list the main results we managed to achieve:


Thanks to introducing the ability for passengers to choose ride types, and giving drivers with more expensive cars the opportunity to work at a higher price point — we managed to improve almost all key metrics.


I hope that my experience will be useful both for those who have already implemented something similar — it would be interesting to know what challenges they faced — and I’m also sure that those who are at the beginning of the journey and still have to go through this will be able to avoid the same rake that I stepped on.


It was a great challenge, during which I stumbled many times, I pushed a faulty release and broke orders. It was a project at the intersection of backend, mobile development, and introducing something completely new into an already established product.


This experience taught me a lot, and I later applied these lessons in other projects.


I wish you to always ride in premium cars when you order a taxi 🥰