Quotes and pricing v1: surge as market clearing
- Assemble a fare quote from base rates, ETA, and a surge multiplier — and make it binding.
- Explain surge as the exchange's price-discovery mechanism, computed per hex cell.
From 0.2: surge is how the exchange balances supply and demand. From 1.3: the Geo facade returns cached cell-pair ETAs. From 1.4: the geo index knows supply per cell — pricing reuses both.
A quote = base fare + rate_per_km × distance + rate_per_min × ETA, all per city and vehicle tier, times a surge multiplier. Surge v1 is refreshingly simple: a periodic job (every 30 s) computes, per hex cell neighborhood, the ratio of open ride requests to available drivers, maps it through a step function to a multiplier (1.0× – 3.0×), smooths it against the previous value to avoid flapping, and publishes the result to a cache that pricing reads. That's it — no ML at this tier. It reappears as a streaming job in 2.10.
The subtle requirement is that a quote is an offer the rider accepts — a mini contract. So quotes are persisted with an id, a price, and a 5-minute expiry; the ride request references the quote id and is honored at that price even if surge moved meanwhile. Legally and UX-wise, re-pricing at request time is a bait-and-switch; architecturally, persisting quotes gives us an audit trail and a natural idempotency anchor for the request flow (1.6).
sequenceDiagram
autonumber
participant R as Rider app
participant GW as API gateway
participant P as Pricing service
participant G as Geo facade
participant S as Surge cache
participant DB as PostgreSQL
R->>GW: quote request {pickup, dropoff, tier}
GW->>P: forward with rider id
P->>G: ETA + distance (cell-pair, mostly cached)
G-->>P: 14 min, 6.2 km
P->>S: surge multiplier for pickup cell
S-->>P: 1.4x
P->>P: fare = (base + km + min rates) × 1.4
P->>DB: persist quote {id, fare, expires in 5 min}
P-->>R: quote id + fare + ETA for all tiers
Note over R: rider taps Request — quote id anchors the price (1.6)
flowchart LR
RD[("Redis geo index — supply per cell")] --> SJ["Surge job — every 30 s"]
TRIP[("Open requests — demand per cell")] --> SJ
SJ --> SM["ratio to multiplier step function + smoothing"]
SM --> SC[("Surge cache — cell to multiplier")]
SC --> P["Pricing service"]
SC --> HEAT["Driver app heatmap — steer supply toward surge"]
Surge computed per single cell is noisy garbage — one request in an empty cell reads as infinite demand. Compute over a neighborhood (the cell plus two rings), smooth over time, and clamp the multiplier. Pricing bugs are trust-destroying incidents, not just technical ones.