Skip to content

Designing the Trip State Machine for a Ride-Hailing Platform

Trip status is the backbone of a ride-hailing platform. Model it loosely and every other subsystem inherits the ambiguity.

Designing the Trip State Machine for a Ride-Hailing Platform

Ask two engineers on the same ride-hailing team what "trip status" means and you will often get two different answers. One is describing what the rider sees. The other is describing a database column. The gap between those two is where a surprising number of production bugs live.

Trip state is the backbone of the platform. Dispatch reads it, both applications render from it, payments key off it, and support staff reconstruct incidents from it. Modelling it loosely means every one of those subsystems inherits the ambiguity.

Two entities, two lifecycles

The most consequential decision is recognising that a trip and a driver are separate things with separate lifecycles.

A trip belongs to the rider's request. It progresses roughly: created, searching, offer pending, matched, driver arriving, in progress, completed. Cancellation is reachable from most of those. An unfulfilled terminal state is reachable from searching, when no driver could be found.

A driver has their own cycle: offline, available, offered, on trip, and back to available.

These are linked but not the same. One trip sitting in searching may cycle three drivers through offered and back to available before a fourth accepts. If you try to express that with a single status field, you end up with states like "searching-but-one-driver-is-considering-it" and the model stops being useful.

Keeping them separate is what allows driver-side failures — declines, timeouts, disconnects — to be handled without touching trip-level logic every time. That distinction does most of the work in driver reassignment.

The server owns the state

Both applications should be treated as consumers of one authoritative state held on the server, not as co-owners of it.

This sounds obvious and is routinely violated, usually for good-faith performance reasons. A driver app optimistically marks a trip as started so the UI feels responsive. A rider app assumes completion when it sees the payment screen. Then a network partition arrives and the two disagree about what happened.

The class of bug this produces is memorable: a trip that is complete for the rider and in progress for the driver, or a driver who cannot go online again because a trip they finished two hours ago never left the in-progress state.

The rule that avoids it is that the device may request a transition and display a state, but never decide one. Optimistic UI is fine as long as it reconciles against the server on the next successful response.

Transitions are the interesting part, not the states

Listing states is easy. The design work is in the edges between them.

For each transition it is worth being explicit about who can trigger it, what must be true beforehand, what happens if it arrives twice, and what happens if it arrives late. That last pair matters more than teams expect on mobile networks: retries are common, so every transition should be idempotent, and a stale transition arriving after the trip has moved on should be rejected rather than applied.

A trip that has already moved to completed should not accept a driver-arriving event that was queued on a phone for ninety seconds. Without an explicit guard, it will.

Cancellation is not one state

"Cancelled" is where under-modelled state machines usually break down, because cancellation means several different things with different consequences.

A rider cancelling before a driver is assigned costs nothing and touches no one else. A rider cancelling after a driver has been travelling toward them for four minutes has a fee implication and an effect on that driver's earnings. A driver cancelling after acceptance should return the trip to searching rather than ending it. An operations cancellation from the admin panel needs to be attributable to a person.

These are not the same event, and flattening them into one status loses the information support and finance will need later. Recording who cancelled, at what point in the lifecycle, and what the resulting fee treatment was costs very little at write time and is close to impossible to reconstruct afterwards.

Compensation when a later step fails

Some failures happen after the trip is essentially over. A payment authorisation fails at completion. A fare recalculation arrives after the receipt was issued. A refund is approved days later.

The instinct is to move the trip backwards into an earlier state. That is usually wrong: it destroys the record of what actually happened and confuses every downstream consumer that already reacted to completion.

The better pattern is to keep the trip's lifecycle terminal and model the financial outcome separately — a payment state that can move to failed, retried, or refunded without rewriting the trip's history. The trip says what happened on the road. The payment record says what happened with the money. Conflating the two is what makes disputes hard to answer six weeks later.

Log the transitions, not just the current value

A single status column tells you where a trip is. It does not tell you how it got there, which is what you need when a rider disputes a charge or an operations lead asks why matching took ninety seconds in one district.

Persisting each transition — with a timestamp, the actor, and the reason — turns the state machine into an audit trail and an analytics source at the same time. It is also what makes dispatch health measurable at all, since reassignment counts and match latency are derived from exactly these events. There is more on that in observability for real-time dispatch.

Starting simple without painting yourself in

An MVP does not need every state on day one. It does need the transitions to be explicit and logged, and it does need trip and driver state kept apart. Those two decisions are cheap at the start and expensive to retrofit, because by the time they hurt there is production data written under the looser model.

Adding a new state later is straightforward. Splitting one field that has been serving two purposes for a year is not.


We build the trip management and dispatch layer of ride-hailing platforms, along with the driver and rider applications that read from it. If you are modelling this for a new platform, or untangling a status field that has grown several jobs, we are happy to talk it through.

  • State Machines
  • Backend
  • Modelling