Um passageiro toca em “Solicitar” e espera um carro em segundos. Por trás desse toque, o sistema precisa saber onde estão agora centenas de milhares de motoristas em movimento, escolher um bom, garantir que nenhum outro passageiro fique com o mesmo motorista e depois acompanhar uma corrida que termina com um pagamento. É uma pergunta favorita em entrevistas de design de sistemas porque mistura um stream em tempo real de alto volume com um núcleo transacional pequeno e rígido.
Este post projeta o backend passo a passo: requisitos e números de capacidade, ingestão de localizações, indexação geoespacial com geohash, S2 e H3, matching e despacho, a máquina de estados da corrida e as falhas que você precisa prever, com um diagrama de arquitetura que você pode copiar.
Requisitos
Funcionais:
- Os motoristas ficam online e transmitem a localização continuamente.
- Os passageiros pedem uma corrida e veem carros próximos e um tempo estimado de embarque.
- O sistema faz o matching entre um passageiro e um motorista, que pode aceitar ou recusar.
- Os dois apps acompanham a corrida ao vivo; a corrida termina com uma tarifa e um pagamento.
Não funcionais:
- Um matching em poucos segundos.
- Localizações de motoristas com no máximo alguns segundos de atraso.
- Nunca atribuir um motorista a dois passageiros.
- Problemas de uma cidade não podem derrubar outras cidades.
Capacidade: os números que moldam o design
Suponha, para uma operação grande, um milhão de motoristas online no pico, cada um enviando uma localização a cada 4 segundos, e 20.000 pedidos de corrida por minuto:
| Grandeza | Cálculo | Resultado |
|---|---|---|
| Atualizações de localização | 1.000.000 / 4 s | 250.000 por segundo |
| Banda de ingestão | 250.000 × ~100 bytes | ~25 MB/s |
| Histórico bruto no ritmo de pico | 25 MB/s × 86.400 s | ~2 TB por dia |
| Pedidos de corrida | 20.000 / 60 s | ~333 por segundo |
Aparecem duas cargas de trabalho muito diferentes. As atualizações de localização têm um volume enorme, mas cada uma é pequena, descartável e substituída 4 segundos depois. Os pedidos de corrida são três ordens de grandeza mais raros, mas cada um precisa ser tratado exatamente uma vez e está ligado a dinheiro. A arquitetura separa os dois.
Ingestão de localizações: guarde só o que é atual
Os apps dos motoristas enviam localizações por uma conexão persistente, como um WebSocket, ou como pequenas requisições HTTPS periódicas. Um gateway de localização autentica o motorista, descarta pontos impossíveis (um salto de 50 km em 4 segundos) e grava cada atualização em um stream como o Kafka, particionado por cidade.
Três consumidores leem o stream:
- O índice geoespacial ao vivo guarda só a última posição de cada motorista disponível, com uma expiração curta, para que um motorista cujo celular parou de responder desapareça depois de, digamos, 30 segundos.
- O acompanhamento da corrida repassa a posição do motorista atribuído para o app do passageiro.
- O armazenamento frio guarda o histórico completo em armazenamento de objetos para análises, modelos de ETA e resolução de disputas.
A decisão-chave é que o índice quente guarda uma linha por motorista, e não uma linha por atualização. Um milhão de motoristas com cerca de cem bytes cada um dá uns 100 MB, pequeno o bastante para ficar em memória por cidade.
Indexação geoespacial
A pergunta do matching é “quais motoristas disponíveis estão perto deste ponto?”. Um índice de banco de dados comum sobre latitude e longitude responde mal a isso, então os sistemas mapeiam as coordenadas para células.
| Índice | Formato da célula | Pontos fortes | Atenção a |
|---|---|---|---|
| Geohash | Retângulos, codificados como prefixo de string | Simples; os prefixos se aninham; o Redis GEO o usa | As células variam de formato; vizinhas podem ter prefixos diferentes |
| S2 | Quadriláteros sobre um cubo projetado | Hierárquico, usado pelo Google | Mais complexo de raciocinar |
| H3 | Hexágonos | Toda vizinha está à mesma distância; 16 resoluções | Hexágonos não se aninham perfeitamente |
O H3, que a Uber abriu como código aberto em 2018, torna a busca por raio uniforme: pegue a célula do passageiro e o anel de células ao redor, depois reúna os motoristas que estão dentro. A resolução importa. Pela tabela de resoluções do H3:
| Resolução H3 | Área média da célula | Comprimento médio da aresta |
|---|---|---|
| 7 | 5,16 km² | 1,41 km |
| 8 | 0,74 km² | 0,53 km |
| 9 | 0,11 km² | 0,20 km |
As resoluções 8 ou 9 servem para cidades densas; áreas rurais precisam de anéis mais largos ou células mais grosseiras.
Se você quer começar simples, o Redis tem comandos geoespaciais embutidos:
GEOADD drivers:sf -122.4194 37.7749 driver:8812
GEOSEARCH drivers:sf FROMLONLAT -122.4100 37.7800 BYRADIUS 2 km ASC COUNT 20 WITHDIST
O GEOSEARCH, disponível desde o Redis 6.2, retorna os 20 motoristas mais próximos em um raio de 2 km, ordenados por distância. Faça sharding primeiro por cidade, a fronteira natural, e divida lugares muito movimentados, como aeroportos ou estádios, em células mais finas.
Matching e despacho
A distância em linha reta é um mau indicador do tempo de embarque: um motorista a 500 m do outro lado de um rio pode estar a 15 minutos. Por isso o matching funciona em duas etapas:
- Geração de candidatos. Consulte o índice geo para obter motoristas disponíveis perto do ponto de embarque, filtrados por tipo de veículo e outras restrições.
- Ranqueamento. Peça a um serviço de ETA os tempos de embarque pela malha viária para os melhores candidatos e ordene por ETA, somando fatores como a direção do motorista.
Fazer o matching de cada pedido na hora com o motorista mais próximo é guloso. Juntar pedidos por um ou dois segundos e resolvê-los juntos como um problema de atribuição minimiza o tempo total de espera dos passageiros. O preço é um atraso curto em troca de matchings melhores nos horários de pico.
Depois, o despacho envia uma oferta a um motorista por vez, com um timeout de cerca de 15 segundos. Em caso de recusa ou timeout, o próximo candidato recebe a oferta.
Nunca atribua um motorista duas vezes
Dois workers de matching podem escolher o mesmo motorista para dois passageiros no mesmo segundo. Evite isso no estado do motorista, não na lógica de matching:
- Guarde o estado de cada motorista (
available,offered,on_trip) com um número de versão. - Uma oferta é uma atualização condicional: defina
offeredsó se o estado ainda foravailablena versão que você leu. - Exatamente um worker vence; o outro passa para o próximo candidato.
- Uma oferta expirada devolve o motorista para
available, de novo de forma condicional.
A mesma ideia de compare-and-set protege o lado do passageiro: um pedido ativo por passageiro.
A máquina de estados da corrida
Uma corrida passa por requested, matching, driver_assigned, driver_arrived, in_progress, completed e paid, com cancelamento possível a partir de vários estados. O serviço de corridas guarda cada corrida em um banco de dados relacional como o Postgres, aplica as transições só quando o estado atual permite e publica cada mudança como um evento para cobrança, notificações, recibos e análises.
Torne cada transição idempotente, com o ID da corrida e a versão do estado como chave. Redes móveis repetem requisições, e um “motorista chegou” entregue duas vezes não pode fazer nada duas vezes. O exemplo de diagrama de estados mostra como desenhar esse ciclo de vida separadamente.
Atualizações ao vivo, ETAs e preços
Enquanto a corrida está ativa, o app do passageiro recebe a posição do motorista por uma conexão push, limitada a cada poucos segundos e suavizada no cliente. O mesmo stream de localizações alimenta o serviço de ETA, que precisa do trânsito ao vivo, e o preço dinâmico, que compara por célula os pedidos com os motoristas disponíveis em uma janela deslizante. O pagamento acontece depois da corrida por meio de um provedor de pagamentos; o diagrama de integração de gateway de pagamento cobre essa parte.
Falhas que você precisa prever
- Os motoristas perdem a conexão. O app envia só o último ponto quando reconecta, e o índice expira os motoristas que ficam em silêncio.
- Um worker de matching cai no meio de uma oferta. As ofertas têm prazo de expiração; o motorista volta para
availablequando a oferta vence. - O cluster de uma cidade falha. Como tudo é particionado por cidade, a falha fica local. As corridas em andamento mantêm o estado no armazenamento durável de corridas.
- GPS falsificado. Verificações de plausibilidade de velocidade e rota no gateway pegam os casos grosseiros.
O diagrama do app de transporte
Conteúdo do diagrama
- City partition: everything inside is per city
- Driver app
- Rider app
- Location gateway
- locations topic
- Live geo index (latest, 30 s TTL)
- Location history
- Matching + dispatch
- ETA service
- Trip service
- Trips (Postgres)
- GPS every 4 s
- 250k/s
- latest position
- every point
- POST /rides
- nearby available drivers
- rank by pickup ETA
- driver assigned (CAS)
- state transitions
- ride offer, 15 s to accept
Como ler:
- A moldura é uma cidade, a fronteira de partição de tudo o que está dentro dela.
- O caminho das localizações passa por cima: app do motorista, gateway de localização, Kafka e, a partir dele, o índice geo no Redis e o repositório de objetos para o histórico.
- O caminho dos pedidos passa por baixo: o app do passageiro chama o matching, que lê candidatos do índice geo, pede ao serviço de ETA os tempos de embarque e envia a oferta ao motorista.
- O serviço de corridas é dono da máquina de estados e a guarda no Postgres.
- O bloco Markdown mostra os números de capacidade.
Desenhar diagramas de design de sistemas em tempo real
- Separe o caminho quente do caminho transacional. Aqui, o stream de localizações e o ciclo de vida da corrida têm volumes, garantias e modos de falha diferentes; desenhe-os como raias separadas.
- Escreva a chave de partição no diagrama. “Particionado por cidade” explica a escala e o isolamento de falhas em três palavras.
- Mostre o que é atual e o que é histórico. O índice das últimas posições e o armazenamento do histórico respondem a perguntas diferentes.
- Rotule as arestas com taxas: 250.000 atualizações por segundo em uma seta mostram aos revisores onde está a carga.
Para uma abordagem geral desses diagramas, veja o exemplo de diagrama de design de sistemas. Conexões push são tratadas no diagrama de arquitetura de app de chat, e o stream em si no diagrama de arquitetura do Kafka.
Monte no Archboard
- Entre com Google ou GitHub e crie um diagrama.
- Adicione uma Moldura do pack Básico com o nome da cidade.
- Adicione dois nós Dispositivo do pack Rede para o app do motorista e o app do passageiro.
- Adicione um Gateway de API do pack Nuvem para o gateway de localização, um nó Kafka do pack Dados para o stream, um nó Redis para o índice geo e um Repositório de objetos para o histórico.
- Adicione três nós Instância de computação do pack Nuvem para os serviços de matching, ETA e corridas, e um nó Postgres do pack Dados para as corridas.
- Conecte o caminho das localizações por cima e o dos pedidos por baixo, e rotule as arestas com o que elas carregam e com que frequência.
- Adicione um bloco Markdown do pack Básico com os números de capacidade.
O exemplo usa 12 nós de diagrama e fica dentro do plano Free.
Perguntas frequentes
Com que frequência os motoristas devem enviar a localização? A cada poucos segundos é uma faixa comum. Atualizações mais frequentes deixam as posições mais frescas, mas multiplicam o volume de ingestão: reduzir o intervalo pela metade, de 4 para 2 segundos, dobra o tráfego.
Devo usar geohash, S2 ou H3? Qualquer um funciona. O geohash é o mais simples e vem embutido no Redis; o H3 dá distâncias uniformes entre vizinhas, o que deixa mais limpas as buscas por raio e o preço por célula. Escolha um e use de forma consistente.
Como evitar atribuir um motorista a dois passageiros? Faça da oferta uma atualização condicional no estado do motorista, para que ela só dê certo se o motorista ainda estiver disponível na versão que o matcher leu. Só um matcher pode vencer.
Por que não fazer o matching de cada passageiro imediatamente com o motorista mais próximo? O matching guloso pode deixar os passageiros seguintes com esperas longas. Agrupar os pedidos por um ou dois segundos e resolvê-los juntos reduz o tempo total de espera, ao custo de um atraso curto.
Que banco de dados guarda as corridas? Um banco de dados relacional encaixa bem: as corridas são transacionais, precisam de transições de estado rígidas e alimentam a cobrança. Os dados de localização de alto volume ficam em outro lugar, em memória e no armazenamento de objetos.