Real-Time Matching (Ride-Hail / Delivery) — Designed in Stages
You don’t need to design for scale on day one.
Define what you need—request ride or order, match driver to rider (or courier to order), track location, ETA, and dispatch—then build the simplest thing that works and evolve as concurrency and geography grow.
Here we use ride-hail or delivery matching (Uber-, Swiggy-style) as the running example: riders/orders, drivers/couriers, trips/deliveries, and location. The same staged thinking applies to any two-sided real-time marketplace where supply (drivers) is matched to demand (riders or orders): low-latency match, geospatial lookup, and consistent trip state are central.
Requirements and Constraints (no architecture yet)
Section titled “Requirements and Constraints (no architecture yet)”Functional Requirements
- Request ride or order — rider or customer creates a request (pickup location, optional destination or drop-off); request enters a queue or is matched immediately.
- Match driver to rider (or order to courier) — assign an available driver/courier to the request; match by proximity, availability, and optionally capacity or type; one driver per active trip.
- Track location — drivers (and optionally riders) send location updates; system stores or streams for ETA and display; riders see driver approaching.
- ETA — estimated time of arrival for driver to pickup and optionally to destination; may use routing service (internal or third-party).
- Dispatch — confirm match to both sides; driver accepts or rejects (optional); trip state (en route, at pickup, in progress, completed); idempotent so retries don’t double-dispatch.
Quality Requirements
- Low latency for match — time from request to assigned driver should be short (e.g. seconds); matching and notification path must be fast.
- Fairness — avoid starvation (e.g. same driver always gets requests); balance load across drivers; optional fairness or priority rules.
- Geospatial matching — find drivers within radius or by grid; distance and availability drive assignment; spatial queries are hot path.
- Consistency of state — trip and driver state (available vs on_trip) must be consistent; no double-assignment of same driver to two trips; no lost requests.
- Expected scale — requests per second, number of active drivers, geographic area (single city vs multi-region).
Key Entities
- Rider / Order — demand side; has pickup location (lat/lon), optional destination, created_at; becomes a trip when matched.
- Driver / Courier — supply side; has current location (lat/lon), status (available, on_trip, offline), optional capacity or vehicle type.
- Trip / Delivery — one request matched to one driver; trip_id, rider_id, driver_id, pickup, drop_off, status (pending, accepted, en_route, at_pickup, in_progress, completed, cancelled); created_at, completed_at.
- Location — point (lat, lon, timestamp); used for driver position, pickup/drop-off, and ETA calculation.
Primary Use Cases and Access Patterns
- Request ride/order — write path; create request; optionally enqueue for matching; return request_id; idempotent by idempotency key.
- Match — read + write; find available drivers near pickup (spatial query); assign one driver; create trip; update driver status; notify both sides.
- Location update — write path; driver (or app) sends position; store or broadcast for ETA and map; high write volume.
- ETA — read path; compute or fetch from routing service (pickup ETA, destination ETA); cache or compute on demand.
- Dispatch and trip lifecycle — write path; accept/reject, status transitions; ensure one active trip per driver; idempotent dispatch.
Given this, start with the simplest MVP: one API, one DB, riders and drivers register location (poll or push), matching via nearest available driver in DB (spatial query or simple radius), create trip and update status—then add real-time location (WebSocket or frequent poll), dedicated matching service with geospatial index, ETA/routing, and idempotent dispatch as load grows.
Stage 1 — MVP (simple, correct, not over-engineered)
Section titled “Stage 1 — MVP (simple, correct, not over-engineered)”Goal
Ship working matching: riders request a ride, drivers are found by proximity, trip is created and both see status. One API, one DB; location via poll or simple push; matching = nearest available driver; single region.
Components
- API — REST or similar; auth (rider, driver); request ride (pickup, optional destination) → create request, run matching, return trip or “searching”; driver location update (POST location); get trip status; driver accept/start/complete/cancel trip. Single server or small cluster.
- DB — riders, drivers (id, current_lat, current_lon, status, updated_at); requests (id, rider_id, pickup_lat, pickup_lon, created_at); trips (id, request_id, rider_id, driver_id, status, created_at). Index drivers by status and location (e.g. bounding box or simple radius via application logic); index trips by driver and status.
- Location — drivers send location periodically (e.g. every 10–30 s via POST); store in drivers table; no real-time stream yet.
- Matching — on request: query drivers where status = available and distance(pickup, driver) < radius; order by distance; take first; in a transaction create trip and set driver status to on_trip. Use DB transaction and row lock on driver to prevent double-assignment.
- Trip lifecycle — driver accepts (or auto-accept at MVP); status transitions: en_route → at_pickup → in_progress → completed; rider and driver poll trip status or use simple long-poll.
Minimal Diagram
Rider app Driver app | | v v+-----------------------------+| API |+-----------------------------+ | | v vDB (drivers, requests, trips) - drivers: location, status - matching: find nearest available → create trip - trip status updatesPatterns and Concerns (don’t overbuild)
- No double-assignment: when matching, lock the chosen driver row (SELECT FOR UPDATE) and create trip in same transaction; only one trip per driver at a time.
- Idempotency: request ride and dispatch with idempotency key so retries don’t create duplicate requests or assignments.
- Basic monitoring: request rate, match latency, trip creation success, driver location freshness.
Why This Is a Correct MVP
- One API, one DB, location via poll or simple push, matching = nearest available driver in DB, trip and status → enough to run a small ride-hail or delivery pilot; easy to reason about.
- Vertical scaling and single region buy you time before you need real-time streams, dedicated matching service, and geospatial index.
Stage 2 — Growth Phase (real-time location, matching service, ETA)
Section titled “Stage 2 — Growth Phase (real-time location, matching service, ETA)”What Triggers the Growth Phase?
- Need lower match latency and fresher driver locations; polling is too slow or load is high.
- Many concurrent requests and drivers; matching logic and spatial queries need a dedicated service and proper geospatial index (grid or R-tree).
- Riders expect ETA and live driver position; need routing service (internal or third-party) and real-time location pipeline.
- Dispatch must be robust under retries and concurrent matches; idempotent dispatch and clear state machine.
Components to Add (incrementally)
- Real-time location updates — drivers send location via WebSocket or frequent POST (e.g. every 5 s); store in DB or in-memory store; matching service reads from same store; optional broadcast to rider app for “driver approaching” map.
- Matching service — dedicated component: consumes queue of requests (or request API calls it); holds pool of available drivers (from DB or cache); uses geospatial index (e.g. grid cells, geohash, or R-tree) to find candidates; assigns driver, creates trip, updates driver status; idempotent (idempotency key per request).
- Geospatial index — index drivers by (lat, lon) and status; query “available drivers in cell or within radius” in sub-second; can be in-memory grid, Redis Geo, or DB spatial index (PostGIS, etc.).
- ETA and routing — call routing API (e.g. OSRM, Google Maps) for pickup ETA and destination ETA; cache short-lived by (origin, dest); return ETA in trip response and optionally push updates.
- Idempotent dispatch — request includes idempotency key; matching service stores key → trip_id; on retry return same trip; driver and trip state transitions are consistent.
Growth Diagram
Rider app Driver app | | v v+-----------------------------+| API (request, trip, status) |+-----------------------------+ | | v vRequest queue Location ingest (WebSocket or frequent POST) | | v v+-----------------------------+| Matching service || - geospatial index || - assign driver, create trip|| - idempotent dispatch |+-----------------------------+ | | v vDB (trips, drivers) Routing service (ETA) | | v vNotify rider & driver (push or poll)Patterns and Concerns to Introduce (practical scaling)
- Driver availability window: when matching, consider only drivers updated in last N seconds (stale = treat as offline or refresh); avoid assigning to disconnected drivers.
- Fairness: round-robin or score by “last trip time” so same driver doesn’t get all requests; or random among top-k nearest.
- Monitoring: match latency (p50, p95), location freshness, ETA accuracy, dispatch idempotency hits.
Still Avoid (common over-engineering here)
- Multi-region and surge pricing until you have multiple cities or demand for dynamic pricing.
- Complex batching (e.g. food delivery multi-stop) until product requires it; single pickup–drop-off first.
- Predictive matching or ML until simple distance + availability is insufficient.
Stage 3 — Advanced Scale (multi-region, surge, batching, fairness)
Section titled “Stage 3 — Advanced Scale (multi-region, surge, batching, fairness)”What Triggers Advanced Scale?
- Operations in multiple cities or countries; need multi-region (match and trip in same region, low latency).
- Surge or dynamic pricing; match and pricing interact (e.g. show price before confirm; driver incentives).
- Delivery batching: one courier serves multiple orders; route optimization and batched assignment.
- Scale: many concurrent requests and drivers; fairness and allocation algorithms; operational SLOs.
Components (common advanced additions)
- Multi-region — route rider and driver to same region (e.g. by city or datacenter); matching and trip state are regional; no cross-region match; replicate or sync only what’s needed for global views (e.g. admin).
- Surge pricing integration — pricing service computes multiplier or price by zone and demand; request flow includes price; driver may see surge zone; match and quote are consistent; see Payments example if collecting payment.
- Batching (e.g. food delivery) — multiple orders assigned to one courier; route optimization (pickup and drop order); matching considers capacity and route; ETA per stop; more complex state (order-level and trip-level).
- Fairness and allocation — avoid starvation; balance earnings or trip count across drivers; allocation algorithm (e.g. max-min fairness, or score = f(distance, last_trip_time)); tune for marketplace health.
- Scale — matching service sharded by region or grid; location pipeline at scale (e.g. stream processing for location updates); DB read replicas for trip status; observability (match latency, acceptance rate, ETA error).
Advanced Diagram (conceptual)
Riders (multi-region) Drivers (multi-region) | | v vAPI (regional routing) | | v vMatching service (per region) Location pipeline (stream or batch) | | v vGeospatial index + fairness Trip state (DB) | | v vSurge / pricing (optional) Routing / ETA | | v vBatching (delivery: multi-stop) Notify rider & driverPatterns and Concerns at This Stage
- Consistency across regions: trip and driver state are regional; define clear boundaries (no cross-region match); global reporting may be eventually consistent.
- Surge and match: price shown to rider should be valid at match time; driver acceptance may be optional with timeout and re-match.
- SLO-driven ops: match latency, trip creation success rate, ETA accuracy, driver availability freshness; error budgets and on-call.
Summarizing the Evolution
Section titled “Summarizing the Evolution”MVP delivers real-time matching with one API, one DB, location via poll or simple push, and matching as “nearest available driver” in a transaction. Trip lifecycle and status updates are correct and consistent. That’s enough to run a small ride-hail or delivery pilot.
As you grow, you add real-time location (WebSocket or frequent updates), a dedicated matching service with geospatial index, ETA and routing, and idempotent dispatch. You keep driver and trip state consistent and avoid double-assignment.
At advanced scale, you add multi-region, surge pricing, optional batching for delivery, and fairness and allocation tuning. You scale matching and location pipeline without over-building on day one.
This approach gives you:
- Start Simple — API + DB, location poll/push, nearest-driver match, trip lifecycle; ship and learn.
- Scale Intentionally — add real-time location and matching service when latency and concurrency demand it; add ETA when product expects it.
- Add Complexity Only When Required — avoid multi-region, surge, and batching until product and geography justify them; keep match correctness and state consistency first.