“Design a ride-sharing platform like Uber.”

Se YouTube é sobre throughput de dados e WhatsApp sobre latência de mensagens, Uber é sobre dados em movimento. Literalmente. Milhões de carros se movendo simultaneamente, e o sistema precisa saber onde cada um está — a cada segundo — pra conectar passageiros e motoristas em tempo real.

O desafio do Uber é único porque combina:

  • Geolocalização em tempo real (milhões de pontos se movendo)
  • Matching otimizado (encontrar o melhor motorista, não apenas o mais próximo)
  • Cálculo de rota e ETA (com condições de tráfego variando)
  • Pricing dinâmico (oferta e demanda flutuando por região e minuto)

Tudo isso com latência perceptível pro usuário de < 3 segundos entre apertar “pedir corrida” e ver o motorista atribuído.

Vamos aplicar o framework.

Fase 1: Esclarecer requisitos

Requisitos funcionais

FuncionalidadeDetalhe
Solicitar corridaPassageiro informa origem e destino, recebe motorista
MatchingSistema encontra motorista ideal (próximo + disponível)
Tracking em tempo realPassageiro vê motorista se movendo no mapa
ETATempo estimado de chegada (pickup e destino)
PricingEstimativa de preço antes de confirmar, surge pricing
HistóricoRegistro de corridas passadas, recibos

Requisitos não-funcionais

RequisitoTarget
Escala100M+ usuários, 5M motoristas ativos
Corridas/dia20 milhões
Latência de matching< 3 segundos
Location updatesA cada 3-4 segundos por motorista ativo
Disponibilidade99.99% (downtime = motoristas sem renda)
ConsistênciaForte pra matching (1 corrida → 1 motorista), eventual pra tracking

Fora do escopo

  • Uber Eats (delivery tem constraints diferentes)
  • Pagamentos (gateway externo)
  • Rating e review
  • Chat in-app entre rider e driver
  • Múltiplas paradas

A restrição que muda tudo

Diferente de YouTube/WhatsApp onde dados são estáticos após serem criados (vídeo uploaded fica no mesmo lugar, mensagem enviada não muda), no Uber os dados se movem. A posição de cada motorista muda a cada 3-4 segundos. Isso invalida cache, torna indexes voláteis, e exige reprocessamento constante.

Fase 2: Estimativas

Location updates (o fluxo mais intenso)

Motoristas ativos simultaneamente (pico): 2.000.000
Update de localização: a cada 4 segundos
Location writes/segundo: 2M / 4 = 500.000/s

Cada update: ~200 bytes (lat, lng, heading, speed, timestamp, driver_id)
Bandwidth de ingestão: 500K × 200 bytes = ~100 MB/s

500 mil writes geoespaciais por segundo. Esse é o heartbeat do sistema.

Matching

Corridas/dia: 20.000.000
Corridas/segundo: 20M / 86.400 ≈ 230/s (média)
Pico (sexta à noite, chuva): 230 × 10 = ~2.300/s

Cada matching precisa:
  1. Query geoespacial: "motoristas disponíveis num raio de 3km"
  2. Ranking: quem é o melhor match
  3. Notify driver: enviar request
  4. Timeout: esperar aceite (10-15s)
  5. Fallback: se recusar, próximo da lista

Storage

Corridas/dia: 20M × ~2 KB metadata = 40 GB/dia
Location history: 500K updates/s × 200 bytes × 86.400s = ~8.6 TB/dia (raw)
  → Geralmente só último ponto é guardado em real-time
  → Histórico é amostrado (1 ponto/30s vs 1/4s) = ~1.4 TB/dia

Fase 3: High-level design

O sistema tem três fluxos principais:

  1. Location ingestion — motoristas reportando posição continuamente
  2. Ride matching — encontrar e atribuir motorista ao passageiro
  3. Real-time tracking — passageiro acompanhando motorista no mapa

Arquitetura geral

┌─────────────────────────────────────────────────────────────────────┐
│                   LOCATION INGESTION (contínuo)                       │
├─────────────────────────────────────────────────────────────────────┤
│                                                                       │
│  Driver App ──(cada 4s)──→ Location Service ──→ Geospatial Index     │
│                                    │                                  │
│                                    ▼                                  │
│                            Location History DB                        │
│                                                                       │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│                       RIDE MATCHING                                    │
├─────────────────────────────────────────────────────────────────────┤
│                                                                       │
│  Rider App ──→ Ride Service ──→ Matching Service                     │
│                     │                │                                 │
│                     │                ├──→ Geospatial Index (query)    │
│                     │                ├──→ ETA Service                  │
│                     │                └──→ Pricing Service              │
│                     │                         │                        │
│                     │                         ▼                        │
│                     │              Notify Driver (WebSocket/Push)      │
│                     │                         │                        │
│                     │                    Accept/Reject                 │
│                     ▼                         │                        │
│                  Ride DB ◄────────────────────┘                       │
│                                                                       │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│                     REAL-TIME TRACKING                                 │
├─────────────────────────────────────────────────────────────────────┤
│                                                                       │
│  Rider App ◄──WebSocket──── Tracking Service ◄──── Location Service  │
│                                                                       │
└─────────────────────────────────────────────────────────────────────┘

Componentes principais

ComponenteResponsabilidade
Location ServiceIngere e armazena posições de motoristas
Geospatial IndexBusca rápida: “quem está perto de X?”
Matching ServiceAlgoritmo de atribuição driver↔rider
ETA ServiceCalcula tempo estimado com base em rota + tráfego
Pricing ServiceCalcula tarifa, surge pricing
Ride ServiceGerencia ciclo de vida da corrida
Tracking ServiceStreaming de posição pro rider durante corrida
Notification ServicePush/WebSocket pra driver aceitar/recusar

Deep Dive 1: Geospatial Index — o coração do sistema

O problema mais importante: dado um ponto (localização do passageiro), encontrar rapidamente todos os motoristas disponíveis num raio.

Naive approach (por que não funciona)

SELECT * FROM drivers
WHERE status = 'available'
  AND ST_Distance(location, POINT(-23.55, -46.63)) < 3000  -- 3km
ORDER BY ST_Distance(location, POINT(-23.55, -46.63))
LIMIT 10;

Com 2 milhões de motoristas e 2.300 queries/segundo no pico, um full table scan com cálculo de distância em cada row é catastrófico. O(N) por query × 2.300 queries/s = impossível.

Solução: dividir o mundo em células

Em vez de calcular distância pra todos os motoristas, pré-organize os motoristas em regiões geográficas. Assim a query vira: “quais motoristas estão na minha célula e nas adjacentes?” — O(1) lookup em vez de O(N) scan.

Opção 1: Geohash

Geohash converte coordenadas (lat, lng) num string que representa uma célula retangular:

(-23.5505, -46.6333) → "6gycfq" (célula de ~1.2km × 0.6km com 6 chars)
(-23.5510, -46.6340) → "6gycfq" (mesma célula! estão próximos)
(-23.5505, -46.5900) → "6gycg5" (célula diferente, mais longe)

Propriedade chave: pontos geograficamente próximos compartilham o mesmo prefixo de geohash. Isso transforma uma query geoespacial numa query de string prefix — algo que qualquer banco faz rápido com B-tree index.

Busca de motoristas próximos:

1. Calcular geohash do passageiro: "6gycfq"
2. Identificar 8 células vizinhas: "6gycfr", "6gycfp", "6gycfn", ...
3. Buscar motoristas nessas 9 células
4. Filtrar por distância real (distância euclidiana/haversine)
5. Rankear

De 2M de motoristas, reduz pra ~50-200 candidatos nas 9 células. De O(N) pra O(1) lookup + O(K) filtro onde K « N.

Opção 2: Quadtree

Uma árvore onde cada nó divide o espaço em 4 quadrantes. Áreas com mais motoristas são subdivididas mais finamente.

        [Mundo inteiro]
       /    |    |    \
    [NW]  [NE]  [SW]  [SE]
                  |
          /   |   |   \
        ...  ...  ...  [São Paulo Centro]
                            |
                    /    |    |    \
                 [Pinheiros][Consolação][Bela Vista][Liberdade]

Vantagens sobre geohash:

  • Adapta granularidade à densidade (centro tem células menores que zona rural)
  • Eficiente pra range queries (percorre a árvore, poda branches fora do raio)

Desvantagens:

  • Mais complexo de implementar e distribuir
  • Rebalanceamento quando densidade muda (eventos, horário de pico)

Opção 3: H3 (Uber’s choice)

O Uber desenvolveu internamente o H3 — um sistema de indexação hexagonal hierárquico.

Por que hexágonos?

  • Quadrados (geohash) têm vizinhos a distâncias desiguais (diagonal > lateral)
  • Hexágonos têm todos os vizinhos à mesma distância do centro
  • Isso elimina edge cases em boundary: “motorista está a 100m mas numa célula ’longe'”
Resoluções H3:
  Res 7: ~5.16 km² (bom pra matching)
  Res 9: ~0.1 km² (bom pra ETA granular)
  Res 12: ~0.003 km² (tracking preciso)

O fluxo com H3:

1. Driver reporta (lat, lng)
2. Sistema calcula H3 index na resolução 9: "892a1008003ffff"
3. Armazena: driver_id → h3_cell mapping
4. Rider pede corrida em (lat, lng)
5. Sistema calcula H3 cell do rider + k-ring (vizinhos num raio k)
6. Busca todos drivers nos cells do k-ring
7. Ranking por distância real + ETA

Implementação do index em memória

Com 2M de motoristas atualizando a cada 4 segundos, o geospatial index precisa ser in-memory. Disco é lento demais pra 500K updates/s.

Estrutura: HashMap<H3Cell, Set<DriverID>>

Update (driver moveu):
  1. Remove driver do cell antigo: cells["892a1008003"].remove("drv_123")
  2. Adiciona no cell novo: cells["892a1008007"].add("drv_123")
  
Query (rider quer corrida):
  1. Calcula k-ring de cells: h3.kRing("892a100800f", k=2)
  2. Pra cada cell no ring: retorna drivers disponíveis
  3. Merge + sort por distância

Sharding do index:

O mundo é grande demais pra um server. Shard por região geográfica:

  • Shard “São Paulo”: cobre cells H3 da região metropolitana
  • Shard “Rio de Janeiro”: cobre cells do Rio
  • Cada shard é um cluster Redis ou serviço in-memory dedicado

Deep Dive 2: Matching Algorithm

Encontrar motorista não é simplesmente “o mais perto”. O matching precisa otimizar múltiplos fatores.

Fatores de ranking

FatorPesoMotivo
Distância (ETA de pickup)AltoPassageiro não quer esperar
Rating do motoristaMédioQualidade do serviço
Direção do movimentoMédioMotorista indo na direção certa é melhor que um parado
Taxa de aceitaçãoMédioMotorista que cancela muito não recebe prioridade
Tipo do veículoFiltroUberX vs Black vs Pool
Tempo ociosoBaixoJustiça: motorista esperando há mais tempo tem prioridade

O fluxo de matching

1. Rider solicita corrida
2. Query geospatial: candidatos num raio de 3-5km
   (retorna ~20-50 drivers)
3. Pra cada candidato, calcula ETA de pickup (ETA Service)
   (filtra: ETA > 15min? descarta)
4. Score = weighted_sum(eta_score, rating_score, direction_score, ...)
5. Envia request pro driver com maior score
   ├──→ Driver aceita (10-15s pra responder)
   │       → Corrida atribuída ✓
   └──→ Driver recusa ou timeout
           → Próximo da lista (step 5 com #2 do ranking)
           → Repete até 3-5 tentativas
           → Se ninguém aceita: "nenhum motorista disponível"

Batch matching vs Sequential matching

Sequential (descrito acima): envia pra um de cada vez. Simples mas lento se muitos recusam.

Batch (Uber’s approach em mercados densos):

Em vez de matchear 1 rider com 1 driver por vez, o sistema acumula requests por 1-2 segundos e resolve como um problema de otimização global:

Riders pedindo: [R1, R2, R3, R4, R5]
Drivers disponíveis: [D1, D2, D3, D4, D5, D6, D7]

Otimização: minimizar ETA total de pickup do batch
  → R1↔D3, R2↔D1, R3↔D7, R4↔D5, R5↔D2

Isso evita conflitos (dois riders querendo o mesmo driver) e encontra o ótimo global em vez de ótimos locais. Na prática, usa-se Hungarian Algorithm ou variantes de Linear Assignment Problem.

Supply positioning (driver heat maps)

O matching não começa quando o rider pede corrida. Começa antes: o sistema sugere pra motoristas onde se posicionar baseado em predição de demanda.

ML Model:
  Input: hora, dia da semana, clima, eventos, dados históricos
  Output: previsão de demanda por cell H3 nos próximos 15 min

Se cell "Aeroporto GRU" vai ter alta demanda em 20min:
  → Notifica motoristas na região: "alta demanda prevista em GRU"

Deep Dive 3: ETA Service

ETA (Estimated Time of Arrival) parece simples mas é surpreendentemente complexo. Não é distância/velocidade.

Por que é difícil?

  • Distância em linha reta ≠ distância de rota (ruas não são linhas retas)
  • Velocidade varia por trecho (avenida ≠ rua local ≠ rodovia)
  • Condições de tráfego mudam a cada minuto
  • Semáforos, obras, acidentes
  • Conversões à esquerda levam mais tempo que à direita
  • Hora do dia (rush hour é 3x mais lento)

Componentes do ETA

┌──────────────────────────────────────────────────┐
│                  ETA Service                       │
├──────────────────────────────────────────────────┤
│                                                    │
│  1. Route calculation (graph algorithm)           │
│     - Road network como grafo (nós = interseções) │
│     - Dijkstra/A* pra shortest path               │
│                                                    │
│  2. Segment speed estimation                       │
│     - Dados históricos por trecho por hora        │
│     - Dados real-time de motoristas ativos        │
│     - Machine learning pra predição              │
│                                                    │
│  3. Aggregation                                    │
│     - Soma tempo de cada segmento da rota         │
│     - Adiciona overhead (semáforos, conversões)    │
│     - Retorna ETA + confidence interval           │
│                                                    │
└──────────────────────────────────────────────────┘

Road network como grafo

Nós (nodes): interseções, curvas, mudanças de velocidade
Arestas (edges): segmentos de rua entre dois nós

Cada edge tem:
  - distance_meters: 342
  - speed_limit_kmh: 60
  - current_speed_kmh: 25 (real-time, inferido de GPS dos drivers)
  - road_type: "arterial" | "local" | "highway"
  - weight (tempo): distance / current_speed = 342/25 × 3.6 = ~49s

São Paulo tem ~150.000 segmentos de rua. O grafo inteiro cabe em memória (~200 MB). Dijkstra com otimizações (A*, contraction hierarchies) resolve rotas em <50ms.

Real-time speed estimation

O Uber tem uma vantagem massiva: milhares de motoristas ativos são sensores de tráfego ambulantes.

Driver D1 percorreu segmento S47 em 85 segundos (distância: 500m)
  → Velocidade observada: 500/85 × 3.6 = 21 km/h

Driver D2 percorreu S47 em 90 segundos
  → Velocidade: 20 km/h

Média ponderada recente (últimos 5 min): 20.5 km/h
Velocidade esperada (sem tráfego): 50 km/h
Congestion factor: 50/20.5 = 2.4x mais lento

Essa informação é agregada em real-time e alimenta o cálculo de ETA. É por isso que o ETA do Uber é geralmente mais preciso que o Google Maps — tem dados de velocidade real de milhares de veículos por segmento.

Caching de ETA

ETAs entre cells H3 populares são pré-calculados e cacheados:

Cache key: eta:{origin_h3}:{destination_h3}:{time_bucket}
Cache value: {eta_seconds: 480, confidence: 0.85, route_polyline: "..."}
TTL: 2-5 minutos (tráfego muda)

Pro matching (que precisa de ETA pra 20-50 candidatos), o cache evita recalcular rotas do zero a cada request.

Deep Dive 4: Location ingestion e tracking

O pipeline de ingestão

500.000 updates por segundo. Não pode passar por um API server tradicional — precisa de um pipeline de streaming.

Driver App (cada 4s)
       ▼ (UDP ou HTTP/2 com batching)
  Load Balancer
  Location Gateway (stateless, alto throughput)
       ├──→ Geospatial Index (in-memory, real-time)
       │      update: move driver de cell A pra cell B
       ├──→ Kafka topic: "driver-locations"
       │      │
       │      ├──→ Consumer: Location History DB (write-optimized)
       │      ├──→ Consumer: ETA Speed Aggregator
       │      └──→ Consumer: Tracking fanout (riders observando)
       └──→ Redis: last_known_location:{driver_id}
              (pra queries pontuais)

Por que Kafka no meio?

  1. Desacoplamento: se o Location History DB ficar lento, não afeta o geospatial index
  2. Múltiplos consumers: mesmo evento alimenta tracking, ETA, analytics, fraud detection
  3. Replay: se um consumer crashar, pode reprocessar do offset

Tracking: rider vendo motorista no mapa

Quando o rider está acompanhando a corrida, precisa ver a posição do motorista atualizar a cada poucos segundos.

Approach: Fan-out on write via WebSocket

1. Rider abre tela de tracking → conecta WebSocket ao Tracking Service
2. Tracking Service subscribe no Kafka topic filtrado por driver_id
3. A cada location update do driver no Kafka:
   → Tracking Service pusha pro rider via WebSocket
4. Rider vê o ícone do carro se movendo no mapa

Escala: durante uma corrida, 1 rider observa 1 driver. Não é fan-out massivo (diferente de presença em WhatsApp). Mas com 5M de corridas ativas simultaneamente, são 5M de WebSocket connections no Tracking Service.

Otimização: client-side interpolation

Enviar posição a cada 4s faz o carro “pular” no mapa. Solução: o client interpola a posição entre updates:

Update T=0:  driver em (-23.550, -46.633), speed=30km/h, heading=90°
Update T=4:  driver em (-23.550, -46.630)

Entre T=0 e T=4, client anima o carro se movendo suavemente
usando speed + heading pra estimar posição intermediária

Isso é chamado dead reckoning — predizer posição baseado em velocidade e direção até o próximo update real. Resultado: animação suave sem aumentar a frequência de updates.

Deep Dive 5: Pricing e Surge

Pricing estático

fare = base_fare 
     + (distance_km × per_km_rate)
     + (duration_min × per_min_rate)
     + booking_fee
     - promotions

Exemplo São Paulo (UberX):
  fare = R$3.00 + (8.5 × R$1.20) + (22 × R$0.15) + R$1.50
       = R$3.00 + R$10.20 + R$3.30 + R$1.50
       = R$18.00

Surge pricing (preço dinâmico)

Quando demanda > oferta numa região, o preço sobe pra:

  1. Incentivar mais motoristas a irem pra região
  2. Desincentivar corridas não-urgentes (equilibrar demanda)

Implementação:

Para cada cell H3 (resolução 7), a cada 2 minutos:

  demand = requests_nos_ultimos_5min
  supply = drivers_disponiveis_no_cell
  
  ratio = demand / supply
  
  if ratio > 1.5:
    surge_multiplier = f(ratio)  -- curva configurável
    -- ex: ratio 2.0 → surge 1.5x
    -- ex: ratio 4.0 → surge 2.5x
  else:
    surge_multiplier = 1.0

  fare_final = fare_base × surge_multiplier

Visualização pro rider: aquele mapa com zonas vermelhas/alaranjadas mostrando onde o surge está ativo.

O dilema do surge

Surge alto demais → riders cancelam → demand cai → surge cai → mais riders pedem → demand sobe → surge sobe… Oscilação.

Solução: smoothing. O surge é calculado como média móvel (últimos 5-10 minutos), não instantâneo. Isso amortece oscilações e torna o preço mais previsível.

Deep Dive 6: Database design

Ride DB (PostgreSQL — relacional)

CREATE TABLE rides (
    id UUID PRIMARY KEY,
    rider_id UUID NOT NULL,
    driver_id UUID,
    status TEXT NOT NULL,
    -- 'requested','matched','driver_en_route','arrived',
    -- 'in_progress','completed','cancelled'
    
    pickup_lat DECIMAL(9,6),
    pickup_lng DECIMAL(9,6),
    dropoff_lat DECIMAL(9,6),
    dropoff_lng DECIMAL(9,6),
    
    pickup_eta_seconds INT,
    estimated_fare_cents BIGINT,
    actual_fare_cents BIGINT,
    surge_multiplier DECIMAL(3,2),
    
    requested_at TIMESTAMP,
    matched_at TIMESTAMP,
    pickup_at TIMESTAMP,
    dropoff_at TIMESTAMP,
    
    INDEX idx_rider (rider_id, requested_at DESC),
    INDEX idx_driver (driver_id, requested_at DESC),
    INDEX idx_status (status)
);

CREATE TABLE drivers (
    id UUID PRIMARY KEY,
    user_id UUID REFERENCES users(id),
    vehicle_type TEXT,        -- 'economy', 'comfort', 'black'
    license_plate VARCHAR(20),
    status TEXT,              -- 'available', 'busy', 'offline'
    current_ride_id UUID,
    rating DECIMAL(2,1),
    acceptance_rate DECIMAL(3,2),
    
    INDEX idx_status (status)
);

Por que SQL? Corridas têm relações claras (rider, driver), precisam de transações ACID (não pode atribuir mesmo driver a dois riders), e queries de histórico com filtros complexos.

Location History (time-series DB — TimescaleDB ou InfluxDB)

CREATE TABLE location_updates (
    driver_id UUID,
    timestamp TIMESTAMPTZ,
    lat DECIMAL(9,6),
    lng DECIMAL(9,6),
    speed_kmh DECIMAL(5,1),
    heading DECIMAL(5,2),
    accuracy_meters DECIMAL(4,1),
    
    -- TimescaleDB: particiona automaticamente por tempo
    PRIMARY KEY (driver_id, timestamp)
);

-- Retenção: 7 dias raw, depois downsample pra 1 update/30s
SELECT set_chunk_time_interval('location_updates', INTERVAL '1 day');

Por que time-series DB? 500K writes/s de dados com timestamp que só cresce. Time-series DBs são otimizados exatamente pra esse pattern: append-only, compressão temporal, queries por janela de tempo.

Geospatial Index (Redis + custom logic)

-- Redis: HashMap por H3 cell
SADD "cell:892a1008003:available" "drv_123" "drv_456" "drv_789"
SADD "cell:892a1008007:available" "drv_012" "drv_345"

-- Quando driver move pra outro cell:
SREM "cell:892a1008003:available" "drv_123"
SADD "cell:892a100800f:available" "drv_123"

-- Query: drivers disponíveis num raio (k-ring):
SUNION "cell:892a1008003:available" "cell:892a1008007:available" "cell:892a100800f:available" ...

O(1) pra cada operação. Redis Cluster com sharding por região geográfica.

Handling de cenários edge

Driver aceita mas não se move

Solução: timeout de pickup. Se o driver não se move em direção ao rider por 2 minutos após aceitar, o sistema:

  1. Pergunta ao driver se está a caminho
  2. Se não responde: cancela automaticamente, reatribui, e penaliza o driver

Detecção: compara posição atual com posição no momento do aceite. Se distância ao pickup não diminuiu, flag.

Dois riders pedem corrida ao mesmo tempo pra mesmo driver

Solução: lock otimista no matching.

1. Matching Service seleciona driver D1 pra rider R1
2. Tenta: UPDATE drivers SET status='busy', current_ride_id='ride_R1' 
         WHERE id='D1' AND status='available'
3. Se affected_rows = 1 → sucesso, driver locked
4. Se affected_rows = 0 → outro matching pegou primeiro → próximo candidato

Compare-and-swap atômico. Sem race condition.

GPS impreciso (túnel, garagem, prédios altos)

Soluções:

  • Snap-to-road: ajustar coordenadas GPS pra rua mais próxima no road graph
  • Kalman filter: suavizar série de posições, remover outliers impossíveis (carro não pode ir de 0 a 200km/h em 1 segundo)
  • Accuracy field: cada update inclui margem de erro. Se accuracy > 50m, ignorar pra matching

Surge descontrolado em evento grande (show, jogo)

100.000 pessoas saindo do estádio ao mesmo tempo:

Demand/min no cell do estádio: 5.000
Supply disponível: 200
Ratio: 25x → surge de 8x? Insustentável.

Solução: cap + redistribuição.

  1. Surge tem cap (máximo 3-5x dependendo do mercado)
  2. Sistema envia incentivos pra motoristas em raio de 10-20km
  3. Fila virtual: “estimativa de espera: 15 minutos” em vez de preço absurdo
  4. Geo-fence do evento: pre-posicionar motoristas antes do evento acabar (ML prevê fim)

Trade-offs e decisões

DecisãoAlternativaPor que essa escolha
H3 (hexagonal)Geohash (retangular)Vizinhos equidistantes, menos edge cases
In-memory geospatial indexPostGIS500K updates/s é muito pra DB on-disk
Sequential matching (padrão)Batch matchingMais simples, bom pra mercados de baixa densidade
Batch matching (mercados densos)SequentialÓtimo global, evita conflitos
WebSocket (tracking)PollingLatência mínima, eficiente pra streaming contínuo
Kafka (location pipeline)Ingestão direta no DBDesacoplamento, múltiplos consumers, replay
Time-series DB (locations)PostgreSQLOtimizado pra append, compressão, janelas temporais
SQL (rides)NoSQLTransações ACID pra matching, queries de histórico
Dead reckoning (client)Updates mais frequentesUX suave sem aumentar carga no server
Surge smoothing (média móvel)Surge instantâneoEvita oscilação de preço

Como escalar além

  1. Predição de demanda: ML que antecipa picos (chuva começando, show acabando) e pre-posiciona supply
  2. Multi-modal: integrar transporte público no ETA (bike → metrô → UberX)
  3. Shared rides (Pool): matching multi-party — otimizar rota que atende N riders com desvio mínimo
  4. Autonomous vehicles: driver é um robô, mas a arquitetura de matching/tracking é a mesma
  5. Cross-region routing: corrida que cruza boundary entre shards geográficos

Resumo

ComponenteTecnologiaMotivo
Geospatial indexRedis + H3 (in-memory)500K updates/s, queries O(1)
Location ingestionKafka pipelineDesacoplamento, múltiplos consumers
Location historyTimescaleDB/InfluxDBTime-series otimizado, compressão
Ride managementPostgreSQLACID, relações, queries complexas
MatchingCustom service + geospatialMulti-factor ranking, batch optimization
ETAGraph + real-time speedsDijkstra/A* com dados de tráfego live
TrackingWebSocket + dead reckoningStreaming de posição, animação suave
PricingRule engine + ML demand forecastSurge dinâmico por cell H3
Driver communicationWebSocket + Push (FCM/APNs)Aceitar/recusar em tempo real

Esse é o quarto artigo da série System Design na Prática. No próximo, vamos projetar o Twitter/X — feed de notícias com fan-out pra centenas de milhões de timelines, caching agressivo, e o dilema de trending topics em tempo real.