Functional Requirements Link to heading

  • Riders can input a start location and destination and get a fare estimate.
  • Riders can request a ride based on the estimated fare.
  • Upon request, riders are matched with a nearby, available driver.
  • Drivers can accept or decline a request and navigate to the pickup and drop-off.

Basic High-Level Design Link to heading

flowchart LR subgraph clients["Clients"] direction TB DC["Driver Client\niOS / Android"] RC["Rider Client\niOS / Android"] end GW["API Gateway\n& Load Balancer\n- Routing\n- Authentication\n- Rate Limiting"] subgraph services["Services"] direction TB RS["Ride Service\n- Fare Estimation\n- Ride Creation\n- Accept / Decline Ride"] RMS["Ride Matching Service\n- Fetch closest drivers\n- Match driver and rider"] LS["Location Service\nWebSocket to Driver"] NS["Notification Service\nAPN / FCM"] end MAP["3rd Party\nMapping Service"] subgraph storage["Storage"] DB[("PostgreSQL")] RIDER["Rider\n- id\n- name\n- contact"] DRIVER["Driver\n- id\n- name\n- vehicle"] RIDE["Ride\n- id\n- riderId\n- driverId\n- fareId\n- status"] FARE["Fare\n- id\n- riderId\n- price\n- source\n- destination"] LOC["Location\n- driverId\n- lat\n- long\n- updatedAt"] end RC --> GW DC --> GW GW -->|"getFareEstimate()\nrequestRide()"| RS GW -->|"acceptOrDeclineRide()"| RS GW -->|"updateLocation()"| LS RS -->|"getFareEstimate(src, dst)"| MAP RS -->|"getRoute(src, dst) after accept"| MAP RS --> DB RS -->|"trigger matching"| RMS RMS --> DB RMS --> NS LS --> DB NS -->|"new ride request"| DC NS -->|"ride status"| RC

Deep Dives Link to heading

Driver location is the highest-throughput write in the system — every active driver pushes an update every ~4 seconds. PostgreSQL is the wrong tool here: row-level locking, WAL overhead, and B-tree indexes on lat/long don’t scale to hundreds of thousands of concurrent updates. Instead, location lives entirely in Redis.

Redis stores geospatial data as a sorted set (ZSET) where each member’s score is a 52-bit integer geohash. Latitude and longitude bits are interleaved into a single integer, so nearby coordinates produce similar scores. Proximity queries become efficient range scans on the sorted set, no spatial index to maintain separately.

# Driver sends a location update
GEOADD drivers:available 151.2093 -33.8688 "driver:42"

# Ride Matching Service finds the 10 nearest available drivers
GEOSEARCH drivers:available
  FROMLONLAT 151.2100 -33.8700
  BYRADIUS 5 km
  ASC COUNT 10
  WITHCOORD

GEOSEARCH returns results sorted by actual distance (not geohash bucket).

Data Flow Link to heading
Driver app (every 4s)
  → WebSocket → Location Service
    → Redis: GEOADD drivers:available <long> <lat> <driverId>

Ride Matching Service (on new ride request)
  → GEOSEARCH drivers:available FROMLONLAT <long> <lat> BYRADIUS 5km ASC COUNT 10
  → for each driver candidate: notify via Notification Service

2. Preventing Duplicate Ride Requests to the Same Driver Link to heading

Without coordination, two simultaneous ride requests could both run GEOSEARCH, get the same nearby driver back, and both send that driver a notification at the same time. The driver would get spammed, and whichever ride they accept would leave the other in a broken state.

The fix is a per-driver distributed lock in Redis using SET NX EX (set if not exists, with TTL):

# Try to acquire lock on driver:42 for 30 seconds
SET lock:driver:42 <rideId> NX EX 30

NX makes this atomic, only one caller wins. The lock value is the rideId so the lock holder can identify and release their own lock if needed.

Data Flow Link to heading
Ride Matching Service receives ride request
  → GEOSEARCH: get 10 nearest available drivers
  → for each driver (closest first):
      SET lock:driver:<id> <rideId> NX EX 30
      → acquired: remove driver from geo set, notify driver, stop
      → failed: driver already locked, try next candidate
  → no candidates acquired: re-enqueue ride request after delay

The 30s TTL covers the driver’s response window. If the driver doesn’t respond in time, the lock expires automatically, and the driver becomes eligible again for the next match attempt.

Final Design Link to heading

flowchart TD subgraph clients["Clients"] RC["Rider App\niOS / Android"] DC["Driver App\niOS / Android"] end GW["API Gateway\n& Load Balancer\n- Routing\n- Authentication\n- Rate Limiting"] subgraph services["Services"] RS["Ride Service\n- Fare estimation\n- Ride creation\n- Accept / Decline\n- WebSocket to rider"] RMS["Ride Matching Service\n- Fetch closest drivers\n- Acquire per-driver lock\n- Retry on TTL expiry"] LS["Location Service\nWebSocket server"] NS["Notification Service\nAPN / FCM"] end MAP["3rd Party\nMapping Service"] subgraph storage["Storage"] DB[("PostgreSQL\nRide · Fare · Rider · Driver")] LDB[("Redis — Location DB\nlat/long per driver")] DL["Redis — Distributed Lock\nper-driver TTL 30s"] end subgraph queues["Queues"] MQ["Match Queue — Kafka\nride requests"] LQ["Location Queue — Kafka\nlocation updates"] end RC <-->|"WebSocket — ride status updates"| GW DC -->|"acceptOrDeclineRide()"| GW DC <-->|"WebSocket — location stream"| GW NS -->|"push: accept / decline prompt"| DC GW --> RS GW --> LS RS -->|"getFareEstimate(src, dst)"| MAP RS -->|"getRoute(src, dst) after accept"| MAP RS --> DB RS -->|"enqueue ride request"| MQ RS -->|"push status updates"| RC MQ -->|"consume"| RMS RMS -->|"fetch closest available drivers"| LDB RMS -->|"tryLock(driverId, TTL=30s)"| DL DL -.->|"TTL expired — retry next driver"| RMS RMS -->|"notify driver"| NS RMS --> DB LS -->|"publish"| LQ LQ -->|"consume"| LDB