Taxi Fleet Platform
A subscription platform that runs a taxi fleet end to end: architecture and development
About the project
- Client: a developer of SaaS for taxi fleets (name not disclosed)
- Industry: Taxi and urban mobility
- Scale of use: the main taxi operators in one of the world's largest cities
- Technologies: Go, Flutter, React, PostgreSQL, Kafka, Redis, ClickHouse
- Status: delivered, in production, being refined
We built a subscription platform that runs a taxi fleet end to end. It covers ordering, dispatching, pricing, payments and rides without internet. It also includes apps for passengers and drivers, and consoles for dispatchers and fleet admins. We designed it and wrote a large part of the code ourselves, and today it runs in production.
A taxi fleet is more than an app with a map. It is a queue of orders and drivers on very different phones. It handles money in three forms: card, cash and terminal. Fares must read the same to passengers and drivers, and dispatchers pick up what automation cannot.
The client needed more than one fleet. They needed a platform they could sell to many fleets as a subscription. Each fleet has its own brand, cities, tariffs, drivers and money. Yet the platform could not depend on one country or vendor. Card acquiring, fiscal receipts, maps, SMS and telephony differ from fleet to fleet.
What we delivered
THE CHALLENGE
- A ride must not depend on connectivity, a payment provider or a map service.
- The same fare must always be calculated and explained the same way.
- One fleet must never see another fleet's data.
THE SOLUTION
- Swappable adapters for every external provider.
- Offline-first driver app with a numbered journal of the ride.
- One pricing formula shared by the server and the app.
- Payment hold that never blocks dispatch.
- The database itself enforces fleet isolation.
THE DESIGN TARGETS
- First offer to a driver within 2 seconds (95th percentile).
- Ride path available 99.95% of the month.
- An hour of offline journal merged in 5 seconds.
- Up to 300,000 orders an hour at peak.
- At most a minute of data loss; a region recovered in 4 hours.
Every external service is a swappable adapter, so a provider failure slows money or messages, never the rides.
The key idea: every external service is a swappable adapter
The design has no built-in country and no default provider. For each fleet, we choose adapters for payments, receipts, maps, SMS, push, e-mail, telephony, document checks and card terminals. In addition, every adapter has a test double, a health check and an automatic circuit breaker.
This does two things at once. First, the platform enters a new market by configuration, not by rewriting code. Second, when a provider fails, money or messages slow down, but the rides go on.
How it works for people
- The passenger orders from the fleet's app, a web page or by phone. Then the passenger sees a price for each car class, follows the car on the map and calls the driver through a masked number. SOS is always one tap away.
- The driver uploads documents once, goes online and receives offers with an earnings estimate. If the connection drops, the app keeps recording the trip.
- The dispatcher sees a live map of up to 2,000 drivers and an order board with timers and alerts. When automation finds no driver, the dispatcher assigns one by hand, and the system logs it.
- The fleet admin sets up cities, zones, car classes and tariffs. Beyond setup, the admin reviews driver documents, runs promotions and starts payouts.
- The platform operator onboards fleets, binds providers and releases the apps. In addition, the operator watches service health.
An order is not a quote
Calculating a price does not create an order. Every confirmed order comes with 3–10 quotes that people only viewed. Writing them all to the main table would make it grow fast. So an order appears in the database only at confirmation. All its states pass through a single state machine, and the platform publishes every change as an event. Apps, consoles, analytics and partners read the same stream, so there is no second version of the truth.
Cancellation rules rely on evidence. For example, a paid cancellation or no-show needs proof that the driver arrived and tried to make contact. If GPS gave no position, the platform does not reject the command. Instead, it accepts the command as unverified and sends it to support for review. It never penalises the passenger automatically.
Dispatching: the first offer in two seconds
The first offer to a driver should go out within 2 seconds (95th percentile). So one matcher serves each city. It searches hexagonal map cells around the pickup point. Then it filters out unsuitable drivers, ranks the rest and sends offers on timers. Ranking weighs more than arrival time:
- Arrival time of the driver at the pickup point
- Fairness: how long the driver has been waiting for work
- Acceptance rate of the offers received
- Rating of the driver
- Penalty for cancelling after accepting
Ranking purely by proximity leaves drivers at the edge of busy areas without work. It also rewards those who accept everything and cancel later. When two drivers accept the same offer, an atomic script in Redis settles the race. The database then confirms the winner. The platform shards cities with tens of thousands of drivers by map cells.
Pricing: one formula everywhere
A pure function with no network access and no clock computes the price. The same library runs on the server and in the driver app, and 500+ test vectors keep the two in agreement. So the offline estimate on the phone matches the server calculation.
- Reproducible: we can rebuild any receipt from its stored inputs. A nightly check recalculates 1% of all fares and 100% of adjusted ones.
- Surge: the platform recomputes it every 30 seconds for each map cell, with smoothing and a limit on the step. The passenger sees the multiplier and the reason. Accessibility classes never get it.
- Fixed by default: the price stays fixed unless the route exceeds the plan by more than 15% and 500 m. Traffic alone never changes it, because the fleet carries that risk.
Money: authorisation never blocks the ride
The card hold goes on once the platform assigns a driver, while the driver is on the way to the passenger. It does not go on at the moment of ordering. If the bank declines, the order moves to a "payment pending" state with a fallback the fleet has allowed: cash or a terminal. So an acquirer outage slows down money, not rides.
All money goes through double-entry bookkeeping. Amounts are whole numbers in minor units, and every movement is a balanced posting. The fleet's money and the platform's revenue sit in separate ledgers. As a result, cash, terminal and card payments all end up in the same books. Finally, a nightly reconciliation with the provider catches discrepancies.
Rides without internet
The driver app writes every point and event to a local journal with a sequence number before it sends anything. If the connection drops, the ride continues. Meanwhile, the meter shows a preliminary amount, and the app accepts only cash or terminal payment. After reconnecting, the app uploads the journal in one session, in batches. Then the server checks it and snaps the track to the road network. Finally, it recalculates the distance and closes the order. An hour of journal should merge in 5 seconds (95th percentile).
The server never takes the phone's amount on trust. It accepts cash collected offline if the amount differs from the server's calculation by no more than 10%. The difference goes to the fleet's loss line, so the driver pays nothing.
FIVE MESSAGE CLASSES
- Login codes
- Messages about the trip
- Receipts and payments
- Operational notices
- Marketing
CONFIRMED DELIVERY
- The app confirms every push.
- Without confirmation, critical messages fall back to SMS.
- SMS is limited by a daily quota.
ONE SOS ROUTE
- Raises an alert for the dispatcher.
- Opens an incident in support.
- If neither works, the app shows a direct dial button and a ready SMS.
Fleets are isolated at the database level
Every row in the database carries its fleet tag, and the database itself enforces this, not the application code. The code is the same at every level of isolation:
- Shared: the default, with the database separating fleets.
- Dedicated database: for large fleets or special requirements.
- Dedicated cluster: a fully isolated stack for regulated cases.
A fleet's data stays in one region. We build each fleet's apps from one codebase under its own brand, and fleets without store accounts use a shared app.
What turned out to be hard
- Money and time never line up. Holds, captures, tips, refunds, payouts and reconciliation agree only through a strict order of events and idempotent commands. The design devotes a section and 30+ decisions to this.
- Offline rides invite disputes. GPS gaps, fake coordinates and a second phone that syncs late produce disputed fares. So we cap interpolation and detour distance, and run a dedicated dispute process.
- A big city is a hot spot. A city with 40,000 drivers online presses on a single matcher. So we design for that load and test it before launch.
- Every provider is separate work. Adapters are interchangeable, but each payment, fiscal and terminal provider needs certification. That drives most time estimates.
- Targets must be proven. We built all figures below into the design as targets, because the client does not publish production measurements.
Scale and targets
Every figure on this page is a design target for the second year of operation (500 fleets, 1 million registered drivers, 100,000 position updates per second). None of them is a measurement.
How we check quality
Every layer has a gate, and nothing reaches production on a promise:
- Unit tests, with 100% coverage on money code
- Protobuf and OpenAPI contracts
- Adapters tested against their test doubles
- An isolation suite with zero leaks on every release
- 14 end-to-end scenarios
- A dispatch simulator on recorded cities
- A weekly load run
- A new dispatch policy or tariff goes live only after a successful simulator run
Operations and data protection
Releases go through GitOps. Services on the ride path roll out as a canary, and the platform rolls them back automatically when errors rise. Likewise, apps roll out in waves: canary, 5%, 25% and 100%. However, a wave stops when stability drops. We encrypt personal data with separate keys for each fleet, and we delete or anonymise it on the user's request. A daily checksum confirms the immutable log of staff actions.
What went into the platform
PASSENGERS AND DRIVERS
- Ordering by app, web page or phone
- Live tracking, chat and masked calls
- Driver shifts, documents and offers
- Recording rides offline, with automatic sync
- SOS and trip sharing
DISPATCHERS AND FLEETS
- Live map and order board with timers
- Manual dispatch and phone orders
- Tariffs, zones and car classes
- Driver documents, payouts and reports
- Platform admin and tenant billing
UNDER THE HOOD
- Matching and dispatching
- Pricing with surge and replay
- Payments, ledger and reconciliation
- Routing, tracking and offline merge
- Notifications and alerts
Result and what's next
We delivered the project to the client. The platform runs in production, and we keep improving it gradually. We do not publish the number of fleets, cities or production load, because that is the client's data. Every number above is a design target. We also do not disclose details of the platform's further development.
Are you building a platform for taxi fleets, or a similar multi-tenant service that handles money and has to work offline? Tell us about the task, and we will show how to break it into services and what to verify first.
SEE MORE OUR CASE STUDIES
Contact us
Please fill out the form, and we will get back to you once your request has been processed.
You can also send your request directly to our email: info@24gen.space
We will respond as soon as possible after receiving your message.





