-1

Projetando o Uber em Escala: Um Guia Completo de System Design

Resumo: Um guia completo de system design para construir uma plataforma de compartilhamento de corridas lidando com milhões de viagens simultâneas, matching geoespacial em tempo real, precificação dinâmica e predição de ETA em escala global.

Publicado: Fevereiro 2026
Tempo de leitura: 50 minutos
Palavras-chave: #SystemDesign #Uber #Ridesharing #Geoespacial #TempoReal #PrecificaçãoDinâmica #Matching #Escalabilidade #EntrevistaTécnica


A cada minuto, 24.000 corridas são iniciadas no Uber ao redor do mundo. Um passageiro em São Paulo abre o aplicativo e, em 4 segundos, o sistema já escaneou milhares de motoristas próximos, calculou ETAs usando dados de trânsito em tempo real, encontrou o motorista ideal e iniciou a corrida. Tudo isso enquanto gerencia 14 milhões de outras viagens simultâneas acontecendo em 72 países.

Esse não é simplesmente um problema de "conectar passageiro ao motorista". Esse é um motor de matching geoespacial em tempo real operando em escala planetária, combinado com um sistema de precificação dinâmica que equilibra oferta e demanda em milhares de micro-mercados simultaneamente.

Neste guia aprofundado, vamos construir o Uber do zero. Você vai entender as decisões arquiteturais exatas que tornam o matching em tempo real possível, por que a indexação geoespacial com hexágonos H3 substituiu as quadtrees, como o surge pricing realmente funciona por dentro, e por que o sistema de pagamentos precisa de garantias de consistência mais fortes do que o sistema de matching.

Vamos projetar o sistema que mudou o transporte urbano para sempre.


Sumário


Análise de Requisitos

Requisitos Funcionais

Lado do Passageiro:

  1. Solicitar uma corrida — Selecionar ponto de embarque/desembarque, escolher tipo de corrida (UberX, Comfort, Black)
  2. Rastreamento em tempo real — Ver a localização do motorista ao vivo no mapa
  3. Estimativa de ETA — Saber quando o motorista chega e a duração da viagem
  4. Estimativa de tarifa — Ver o preço antes de confirmar
  5. Histórico de corridas — Viagens anteriores, recibos, avaliações
  6. Pagamentos — Múltiplos métodos de pagamento, divisão de tarifa
  7. Avaliação e feedback — Avaliar o motorista após a viagem

Lado do Motorista:

  1. Ficar online/offline — Alternar disponibilidade
  2. Aceitar/recusar solicitações — Decisão com tempo limitado (15 segundos)
  3. Navegação — Direções curva a curva
  4. Painel de ganhos — Ganhos em tempo real, resumos diários/semanais
  5. Reporte de localização — Atualizações GPS contínuas enquanto online

Plataforma:

  1. Motor de matching — Conectar passageiros aos motoristas ideais
  2. Precificação dinâmica — Ajustar preços baseado em oferta/demanda
  3. Otimização de despacho — Minimizar tempos de espera pela cidade
  4. Detecção de fraude — Identificar viagens fraudulentas, GPS falso
  5. Analytics — Dashboard de operações por cidade

Requisitos Não-Funcionais

RequisitoAlvoJustificativa
Latência de Matching< 30 segundosPassageiro espera match quase instantâneo
Frequência de Atualização de LocalizaçãoA cada 4 segundosAnimação suave no mapa, ETA preciso
Precisão do ETA±2 minutosConfiança e confiabilidade
Disponibilidade99,99%Transporte é crítico para segurança
Consistência de PagamentoForte (ACID)Dinheiro não pode ser perdido ou duplicado
Consistência de MatchingEventualBreve inconsistência é aceitável
Precisão GPS< 10 metrosLocalização de embarque precisa

Estimativas de Escala

Usuários Ativos Mensais: 130 milhões (passageiros + motoristas)
Passageiros Ativos Diários: 25 milhões
Motoristas Ativos (simultâneos): 5 milhões
Corridas por dia: 24 milhões (24K corridas iniciando por minuto)
Atualizações de localização dos motoristas: 5M motoristas × 15 atualizações/min = 75M atualizações/min = 1,25M/seg
QPS de pico para matching: ~50.000 solicitações de corrida/seg
Cidades: 10.000+ em 72 países

Cálculos de Envelope

Antes de mergulhar na arquitetura, precisamos ter uma noção clara dos volumes que o sistema vai precisar suportar. Esses cálculos "de guardanapo" são fundamentais em entrevistas de system design e ajudam a guiar as escolhas tecnológicas que faremos adiante.

Armazenamento

Registros de corridas:
  24M corridas/dia × 2KB por corrida = 48 GB/dia
  Anual: 48 GB × 365 = ~17,5 TB/ano

Localizações de motoristas (dados quentes — apenas últimos 30 minutos):
  5M motoristas × 1 atualização/4seg × 100 bytes = 500M × 100B = 50 GB
  (Mantido em memória/Redis, não persistido a longo prazo)

Histórico de localização (armazenamento frio para analytics):
  1,25M atualizações/seg × 100 bytes × 86.400 seg = ~10,8 TB/dia
  (Armazenado em formato colunar, comprimido ~5x = ~2 TB/dia)

Dados de mapa:
  Grafo da rede viária: ~200 GB (global)
  Pontos de interesse: ~50 GB

Largura de Banda

Atualizações de localização dos motoristas (entrada):
  1,25M/seg × 100 bytes = 125 MB/seg (gerenciável)

Servir tiles de mapa:
  25M passageiros × média de 20 carregamentos de tiles/sessão = 500M tiles/dia
  Tile médio: 50 KB → 25 TB/dia (servido via CDN)

Requisição/resposta de corrida:
  50K/seg × 1KB = 50 MB/seg

Conexões WebSocket (rastreamento em tempo real):
  30M conexões simultâneas × 500 bytes/seg = 15 GB/seg

Esses números podem parecer assustadores à primeira vista. Mas a boa notícia é que, com as tecnologias certas e uma arquitetura bem pensada, cada um desses desafios tem uma solução elegante. Vamos explorar exatamente isso nas próximas seções.


Arquitetura de Alto Nível

graph TB
    subgraph "Client Layer"
        RA[Rider App<br/>iOS/Android]
        DA[Driver App<br/>iOS/Android]
    end

    subgraph "Edge Layer"
        CDN[CDN<br/>Map Tiles, Static Assets]
        LB[Load Balancer<br/>L4/L7]
        WS[WebSocket Gateway<br/>Real-Time Communication]
    end

    subgraph "API Layer"
        AG[API Gateway<br/>Auth, Rate Limiting]
    end

    subgraph "Core Services"
        RS[Ride Service<br/>Trip Management]
        MS[Matching Service<br/>Driver-Rider Matching]
        LS[Location Service<br/>GPS Processing]
        PS[Pricing Service<br/>Surge & Fare Calc]
        ES[ETA Service<br/>Arrival Prediction]
        PAY[Payment Service<br/>Billing & Payouts]
        NS[Notification Service<br/>Push & SMS]
        US[User Service<br/>Profiles & Auth]
        DS[Dispatch Service<br/>City-Level Optimization]
    end

    subgraph "Geospatial Layer"
        GI[Geospatial Index<br/>H3 Hexagonal Grid]
        RT[Route Engine<br/>OSRM/Valhalla]
        MP[Map Service<br/>Mapbox/Google Maps]
    end

    subgraph "Data & Messaging"
        KF[Apache Kafka<br/>Event Streaming]
        RD[(Redis Cluster<br/>Location Cache<br/>Geospatial Index)]
        PG[(PostgreSQL<br/>Users, Trips)]
        CS[(Cassandra<br/>Location History)]
        ES2[(Elasticsearch<br/>Search & Analytics)]
        S3[(S3<br/>Receipts, Logs)]
    end

    RA & DA --> CDN
    RA & DA --> LB
    LB --> AG
    LB --> WS
    AG --> RS & MS & PS & ES & PAY & US
    WS --> LS & NS

    DA -->|GPS every 4s| WS
    WS --> LS
    LS --> RD
    LS --> KF

    MS --> GI
    GI --> RD
    MS --> ES
    ES --> RT

    RS --> KF
    KF --> PAY & NS & DS

    RS --> PG
    LS --> CS
    PAY --> PG

    style MS fill:#e1f5fe
    style GI fill:#fff3e0
    style PS fill:#fce4ec
    style KF fill:#f3e5f5

Por Que Esta Arquitetura?

  1. O WebSocket Gateway é separado do API Gateway REST porque as atualizações de localização dos motoristas são de alta frequência, baixa latência e bidirecionais. REST não dá conta de 1,25M atualizações de localização por segundo.

  2. O Índice Geoespacial está em memória (Redis) porque as posições dos motoristas mudam a cada 4 segundos. Consultas geoespaciais baseadas em disco seriam lentas demais.

  3. Matching e Pricing são serviços separados porque escalam de forma diferente — matching é CPU-bound (computação geoespacial), pricing é I/O-bound (agregação de dados de mercado).

  4. Kafka como backbone de eventos desacopla o caminho de escrita em tempo real (atualizações de localização, eventos de corrida) dos consumidores downstream (analytics, cobrança, notificações).

Pense nessa arquitetura como uma orquestra: cada serviço é um instrumento diferente, tocando sua parte, mas todos sincronizados pelo Kafka que atua como o maestro. Se o violino (serviço de pricing) desafina, os demais instrumentos continuam tocando normalmente. Esse é o poder do desacoplamento.


Design da API

Endpoints REST Principais

// ===== Ride Service API =====

interface RideRequest {
  rider_id: string;
  pickup: GeoPoint;                // { lat: number, lng: number }
  dropoff: GeoPoint;
  ride_type: 'UberX' | 'Comfort' | 'Black' | 'XL' | 'Pool';
  payment_method_id: string;
  scheduled_at?: string;           // Para corridas agendadas (ISO 8601)
}

interface RideEstimate {
  ride_type: string;
  fare_estimate: {
    min: number;                   // em centavos
    max: number;
    currency: string;
    surge_multiplier: number;
  };
  eta_pickup: number;              // segundos até o motorista chegar
  eta_dropoff: number;             // duração total da viagem em segundos
  distance_meters: number;
}

interface Ride {
  id: string;
  status: RideStatus;
  rider: UserSummary;
  driver?: DriverSummary;
  pickup: GeoPoint;
  dropoff: GeoPoint;
  route?: GeoPoint[];              // polilinha da rota
  fare?: FareBreakdown;
  created_at: string;
  started_at?: string;
  completed_at?: string;
}

type RideStatus =
  | 'REQUESTING'      // Passageiro enviou a solicitação
  | 'MATCHING'        // Procurando um motorista
  | 'DRIVER_ASSIGNED' // Motorista aceitou, a caminho do embarque
  | 'ARRIVING'        // Motorista a 1 min do embarque
  | 'WAITING'         // Motorista no ponto de embarque, aguardando passageiro
  | 'IN_PROGRESS'     // Viagem iniciada
  | 'COMPLETED'       // Viagem finalizada
  | 'CANCELLED';      // Cancelada pelo passageiro ou motorista

// POST   /api/v2/rides/estimate    → RideEstimate[]
// POST   /api/v2/rides             → Ride (cria solicitação de corrida)
// GET    /api/v2/rides/:id         → Ride
// PUT    /api/v2/rides/:id/cancel  → Ride
// POST   /api/v2/rides/:id/rate    → void

// ===== Driver API =====

interface DriverStatus {
  driver_id: string;
  status: 'OFFLINE' | 'AVAILABLE' | 'EN_ROUTE' | 'ON_TRIP';
  location: GeoPoint;
  heading: number;                 // graus da bússola
  speed: number;                   // m/s
  vehicle: VehicleInfo;
}

// PUT  /api/v2/drivers/status       → { status: 'AVAILABLE' | 'OFFLINE' }
// POST /api/v2/drivers/location     → void (alta frequência, preferir WebSocket)

// ===== Atualizações de Localização (WebSocket) =====

interface LocationUpdate {
  driver_id: string;
  location: GeoPoint;
  heading: number;
  speed: number;
  accuracy: number;                // precisão GPS em metros
  timestamp: number;               // Unix milissegundos
  battery_level?: number;
}

// WebSocket: wss://location.uber.com/v2/stream
// Motorista envia LocationUpdate a cada 4 segundos
// Servidor envia atualizações de corrida, novas solicitações

Um detalhe importante sobre o design dessa API: repare que separamos as estimativas de tarifa (estimate) da criação efetiva da corrida. Isso não é por acaso. Quando o passageiro abre o aplicativo na Avenida Paulista e digita "Aeroporto de Guarulhos", ele quer ver o preço estimado antes de confirmar. Essa separação permite que o front-end faça múltiplas chamadas de estimativa sem criar corridas fantasma no sistema.

Rate Limiting

const rateLimits = {
  ride_request: { max: 10, window: '1m', per: 'rider' },
  ride_estimate: { max: 60, window: '1m', per: 'rider' },
  location_update: { max: 20, window: '1m', per: 'driver' }, // ~1 a cada 3s
  driver_status: { max: 10, window: '1m', per: 'driver' },
};

O rate limiting é a primeira linha de defesa contra abusos e também protege o sistema de sobrecarga acidental. Imagine um bug no aplicativo do motorista que envia atualizações de GPS 100 vezes por segundo em vez de uma a cada 4 segundos. Sem rate limiting, um único dispositivo com defeito poderia sobrecarregar o pipeline de localização inteiro.


Modelagem de Dados

Diagrama Entidade-Relacionamento

erDiagram
    RIDER {
        bigint id PK
        varchar name
        varchar email
        varchar phone
        float rating
        int trip_count
        timestamp created_at
    }

    DRIVER {
        bigint id PK
        varchar name
        varchar email
        varchar phone
        float rating
        varchar license_number
        varchar status "OFFLINE|AVAILABLE|ON_TRIP"
        jsonb current_location "lat, lng, heading"
        timestamp created_at
    }

    VEHICLE {
        bigint id PK
        bigint driver_id FK
        varchar make
        varchar model
        int year
        varchar color
        varchar license_plate
        varchar type "UberX|Comfort|Black|XL"
    }

    RIDE {
        bigint id PK
        bigint rider_id FK
        bigint driver_id FK
        varchar status
        point pickup_location
        point dropoff_location
        varchar pickup_address
        varchar dropoff_address
        int fare_cents
        varchar currency
        float surge_multiplier
        float distance_meters
        int duration_seconds
        int rating_by_rider
        int rating_by_driver
        timestamp requested_at
        timestamp matched_at
        timestamp started_at
        timestamp completed_at
    }

    PAYMENT {
        bigint id PK
        bigint ride_id FK
        bigint rider_id FK
        int amount_cents
        varchar currency
        varchar status "PENDING|CHARGED|REFUNDED"
        varchar payment_method_id
        timestamp created_at
    }

    LOCATION_HISTORY {
        bigint driver_id
        timestamp recorded_at
        double latitude
        double longitude
        float heading
        float speed
        float accuracy
    }

    RIDER ||--o{ RIDE : "solicita"
    DRIVER ||--o{ RIDE : "dirige"
    DRIVER ||--|| VEHICLE : "possui"
    RIDE ||--|| PAYMENT : "gera"
    DRIVER ||--o{ LOCATION_HISTORY : "reporta"

Um ponto importante sobre essa modelagem: repare que a tabela RIDE armazena tanto pickup_location (ponto geográfico) quanto pickup_address (endereço textual). Isso é intencional. O ponto geográfico é usado para cálculos de matching e roteamento, enquanto o endereço textual é exibido para o passageiro e o motorista na interface do aplicativo. São preocupações diferentes que coexistem no mesmo registro.

Outro detalhe relevante: os valores monetários são armazenados em centavos (fare_cents, amount_cents). Nunca use float ou double para dinheiro. Parece básico, mas a quantidade de sistemas em produção que fazem isso errado é surpreendente. Um centavo perdido por arredondamento, multiplicado por 24 milhões de corridas diárias, vira um buraco financeiro considerável.


Indexação Geoespacial: A Fundação

Este é o coração da arquitetura do Uber. Tudo depende de responder uma pergunta de forma eficiente: "Quais motoristas disponíveis estão perto desta localização?"

Vamos explorar quatro abordagens diferentes, cada uma com seus prós e contras, para entender por que o Uber chegou à solução que usa hoje.

Abordagem 1: Consulta por Raio (Ingênua)

A abordagem mais simples que vem à mente: fazer uma consulta SQL com filtro geográfico.

-- Encontrar todos os motoristas disponíveis dentro de 3km do embarque
SELECT * FROM drivers
WHERE status = 'AVAILABLE'
AND ST_DWithin(
  current_location,
  ST_MakePoint(-46.6388, -23.5489),  -- Embarque em São Paulo (Av. Paulista)
  3000  -- raio de 3km
)
ORDER BY ST_Distance(current_location, ST_MakePoint(-46.6388, -23.5489))
LIMIT 10;

Problema: Com 5 milhões de motoristas ativos, mesmo com um índice espacial do PostGIS, essa consulta é lenta demais a 50K requisições/segundo. O banco de dados se torna o gargalo. Imagine o horário de pico em São Paulo, com milhares de pessoas saindo do trabalho na região da Faria Lima e Paulista, todas pedindo Uber simultaneamente. O PostgreSQL simplesmente não aguenta esse volume de consultas geoespaciais em tempo real.

Abordagem 2: Quadtree

Uma quadtree divide recursivamente o espaço 2D em quatro quadrantes. Cada nó folha contém um número gerenciável de motoristas.

graph TB
    R["World<br/>(Root)"]
    R --> NW["NW Quadrant"]
    R --> NE["NE Quadrant"]
    R --> SW["SW Quadrant"]
    R --> SE["SE Quadrant"]

    NW --> NW1["NW.NW"]
    NW --> NW2["NW.NE<br/>Driver A<br/>Driver B"]
    NW --> NW3["NW.SW<br/>Driver C"]
    NW --> NW4["NW.SE"]

    NE --> NE1["NE.NW<br/>Driver D<br/>Driver E<br/>Driver F"]
    NE --> NE2["NE.NE"]
    NE --> NE3["NE.SW"]
    NE --> NE4["NE.SE<br/>Driver G"]

    style NW2 fill:#e8f5e9
    style NW3 fill:#e8f5e9
    style NE1 fill:#e8f5e9
    style NE4 fill:#e8f5e9
class QuadTree {
  private boundary: BoundingBox;
  private drivers: Driver[] = [];
  private children: QuadTree[] | null = null;
  private readonly MAX_DRIVERS = 50;

  insert(driver: Driver): boolean {
    if (!this.boundary.contains(driver.location)) return false;

    if (this.drivers.length < this.MAX_DRIVERS && !this.children) {
      this.drivers.push(driver);
      return true;
    }

    if (!this.children) this.subdivide();

    for (const child of this.children!) {
      if (child.insert(driver)) return true;
    }
    return false;
  }

  queryRange(range: BoundingBox): Driver[] {
    const found: Driver[] = [];

    if (!this.boundary.intersects(range)) return found;

    for (const driver of this.drivers) {
      if (range.contains(driver.location)) {
        found.push(driver);
      }
    }

    if (this.children) {
      for (const child of this.children) {
        found.push(...child.queryRange(range));
      }
    }

    return found;
  }

Versão adaptada ao limite de caracteres do TabNews. O artigo completo está no blog: https://lemon.dev.br/pt/blog/uber-system-design

Carregando publicação patrocinada...