What I Learned from Giving People a Choice in Ride Types

Written by nualimov | Published 2025/12/15
Tech Story Tags: software-architecture | programming | system-design | distributed-systems | microservice-architecture | legacy-code | monolith-vs-microservices | tech-debt

TLDRHow I redesigned a ride-hailing order form for 360M users inside a 7-year-old monolith. Lessons on legacy code, user habits, and breaking production.via the TL;DR App

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:

  • Give passengers the ability to choose the type of transport based on their needs and their budget
  • Give drivers with premium cars the ability to take more expensive orders
  • Completely redesign the order form, improve UI/UX
  • Improve and update the codebase
  • Create the possibility for future expansion of the order form, adding new types and more complex logic


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:

  • An outdated framework, so we had to write a lot of code manually, because the old libraries didn’t support simplified syntax
  • Legacy: many parts of the code where it wasn’t clear why something was done the way it was, and we had to navigate through it almost blindly
  • Risky deployments, because even with feature flags, the rollout happened live under heavy load


The advantages we got from working inside the monolith:

  • Simpler architecture: the code was written in one service, nothing had to be configured, there were no new services
  • We could consult with the old developers, understand how things were best done, since the first version of the order form was already in the code
  • The monolith already handled the load, so there were no questions about scaling and maintaining high RPS in new services


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:

  • Whether ride-type selection is enabled (in some countries it isn’t available), which also allowed us to roll out the feature in waves
  • Which ride types are enabled (economy, premium, etc.)
  • Whether additional order options are available (child seat, order comments, etc.)


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:

  • Legacy. As I mentioned above, the work was happening right in the core of the system, written more than 7 years ago, which led to very high costs for introducing new code.
  • Old cars. Drivers with cheap and old vehicles were sometimes receiving expensive orders. We were removing this possibility, so a predictable wave of dissatisfaction appeared.
  • User habits. The new form, although more convenient both in UI and in UX, was something users accepted very reluctantly, as they are usually very conservative.
  • New order flows. It was decided to expand the form and the ride types not just to standard or premium, but also to couriers, food delivery, and intercity trips.


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.


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:

  • Don’t be afraid. The monolith was written in a language I barely knew. It’s fine. You can learn anything if you really want to.
  • Motivation. It’s important that you genuinely want to do it. I was truly excited about the idea of the new form, I felt the drive, and maybe that’s why I was able to assemble the product almost on my knees.
  • Don’t overcomplicate. I didn’t try to invent anything complex — essentially I wrote everything as simply as possible, and this simplicity is what allowed us to later expand the functionality.
  • Look at competitors. Copying is normal — there is always someone who has already walked this path and made mistakes there. You can adopt their experience and make it better.


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 🥰


Written by nualimov | Engineering Leader on a fast growth trajectory with 12+ years in product-led tech companies.
Published by HackerNoon on 2025/12/15