Definition
- Geospatial index : encodes locations for fast nearby queries (geohash, Redis GEO commands).
- Driver matching : picks the best available driver by shortest ETA with an atomic claim.
- Surge pricing : dynamic fare multiplier based on the ratio of open requests to available drivers.
- Trip state machine : enforces legal transitions (REQUESTED -> MATCHED -> ARRIVED -> IN_PROGRESS -> COMPLETED).
- Atomic claim : marks a driver as busy in one indivisible operation so two riders cannot be promised the same car.
National Bank of India runs a fleet of cash-delivery vans. The dispatch system always knows where every van is on a live map, finds the closest free van, tells the branch it will arrive in eight minutes, charges more when every van is busy during a rush, and tracks the delivery through stages. Replace vans with cars and branches with riders.
Understanding the Concepts
How does location tracking work?
Each driver's app sends GPS coordinates every few seconds. A geospatial index like geohash or Redis GEO enables fast radius queries. At National Bank of India's cash-van dispatch, every van broadcasts its position so the system always has a live view of the fleet.
package com.bank.rideshare;
import java.util.List;
public class LocationService {
public void updateLocation(String driverId, double lat, double lng) {
geoIndex.update(driverId, lat, lng);
}
public List<String> nearbyDrivers(double lat, double lng, double radiusKm) {
return geoIndex.nearby(lat, lng, radiusKm);
}
}
How does driver matching work?
Pick the nearest available driver by shortest ETA. The claim must be atomic so two riders never get the same driver. The system iterates through nearby drivers in order of proximity and claims the first available one.
package com.bank.rideshare;
public class MatchingService {
public String match(double riderLat, double riderLng) {
var nearby = locationService.nearbyDrivers(riderLat, riderLng, 5.0);
for (String driverId : nearby) {
if (driverStore.claimAtomic(driverId)) return driverId; // atomic!
}
throw new NoDriverAvailableException();
}
}
How does surge pricing work?
Surge multiplier is derived from the ratio of open requests to available drivers in a geographic cell. When demand exceeds supply, the fare multiplier rises to discourage marginal demand and attract more drivers. The multiplier is capped to avoid extreme fares.
package com.bank.rideshare;
import java.math.BigDecimal;
public class SurgePricing {
public BigDecimal multiplier(int openRequests, int availableDrivers) {
if (availableDrivers == 0) return new BigDecimal("3.0");
double ratio = (double) openRequests / availableDrivers;
if (ratio <= 1.0) return BigDecimal.ONE;
return BigDecimal.valueOf(Math.min(3.0, 1.0 + (ratio - 1.0)));
}
}
How does the trip state machine work?
The state machine enforces legal transitions. Each transition triggers side effects like notifications and payment. An illegal transition (e.g., jumping from REQUESTED to COMPLETED) is rejected. This prevents bugs where a trip is billed without the driver ever arriving.
package com.bank.rideshare;
import java.util.Map;
import java.util.Set;
public class TripStateMachine {
private static final Map<TripState, Set<TripState>> ALLOWED = Map.of(
TripState.REQUESTED, Set.of(TripState.MATCHED, TripState.CANCELLED),
TripState.MATCHED, Set.of(TripState.ARRIVED, TripState.CANCELLED),
TripState.ARRIVED, Set.of(TripState.IN_PROGRESS),
TripState.IN_PROGRESS, Set.of(TripState.COMPLETED)
);
public void transition(Trip trip, TripState newState) {
if (!ALLOWED.getOrDefault(trip.state(), Set.of()).contains(newState))
throw new IllegalStateTransitionException(trip.state(), newState);
trip.setState(newState);
sideEffects.trigger(trip, newState); // notify, bill, rate
}
}
How does ETA calculation work?
Use a routing engine over the road network with live traffic data, not straight-line distance. The ETA considers road segments, speed limits, current congestion, and turn costs. A straight-line distance of 2 km might be a 15-minute drive in heavy traffic.
package com.bank.rideshare;
public class ETAService {
public int estimateMinutes(double fromLat, double fromLng, double toLat, double toLng) {
Route route = routingEngine.findFastest(fromLat, fromLng, toLat, toLng);
return (int) Math.ceil(route.durationSeconds() / 60.0);
}
}
How do you handle cancellation?
Cancellation is allowed from REQUESTED and MATCHED states with no fee. Late cancellation (after the driver has arrived) incurs a fee to compensate the driver's time and fuel. The state machine enforces which states allow cancellation.
What about fare calculation?
The fare combines base fare, per-km rate, per-minute rate, and the surge multiplier. All amounts use BigDecimal to avoid floating-point errors in billing. The final fare is computed after trip completion using the actual route distance and duration.
package com.bank.rideshare;
import java.math.BigDecimal;
public class FareCalculator {
public BigDecimal calculate(double distanceKm, int durationMin, BigDecimal surgeMultiplier) {
BigDecimal base = new BigDecimal("50.00");
BigDecimal perKm = BigDecimal.valueOf(distanceKm).multiply(new BigDecimal("12.00"));
BigDecimal perMin = BigDecimal.valueOf(durationMin).multiply(new BigDecimal("2.00"));
return base.add(perKm).add(perMin).multiply(surgeMultiplier);
}
}
What about data consistency?
Location data is best-effort: tolerate missed GPS pings and stale positions. But driver claims and billing must be atomic and correct. Separating these two consistency requirements is the key architectural decision.
What should a senior engineer know?
A senior engineer separates the best-effort location pipeline from the correctness-critical matching and billing paths, tolerating missed GPS pings without failing trips. They also design geospatial sharding by region, handle cross-region trips gracefully, and plan for driver incentive programs that adjust surge thresholds dynamically.
Visual Overview
Ride-Sharing Architecture
┌──────────┐ GPS every 3s ┌──────────────┐
│ Driver │───────────────►│ Geo Index │
│ App │ │ (Redis GEO) │
└──────────┘ └──────┬───────┘
│ nearby query
┌──────────┐ ride request ┌──────▼───────┐
│ Rider │───────────────►│ Matching │
│ App │ │ Service │
└──────────┘ │ (atomic claim│
│ on driver) │
└──────┬───────┘
│
Trip State Machine:
REQUESTED ──► MATCHED ──► ARRIVED ──► IN_PROGRESS ──► COMPLETED
│ │ │
└── CANCELLED └── CANCELLED Payment + Rating
Surge pricing:
open_requests / available_drivers = ratio
ratio <= 1.0 -> 1.0x (no surge)
ratio > 1.0 -> min(3.0, 1 + ratio - 1) surge
Key Takeaway
A ride-sharing system is real-time and location-heavy, built around geospatial indexing, atomic driver matching, routing-based ETA, surge pricing, and a strict trip state machine. The atomic driver claim is the correctness linchpin: one driver must never be offered to two riders.
Location data is best-effort (tolerate missed pings), but matching and billing demand strict correctness. Separating these concerns is what keeps the platform both responsive and trustworthy.
Remember one sentence: index driver locations geospatially, match with an atomic claim, price by live supply-demand ratio, and enforce a strict trip state machine from request to completion.
---