Un pasajero toca «Solicitar» y espera un coche en segundos. Detrás de ese toque, el sistema tiene que saber dónde están ahora mismo cientos de miles de conductores en movimiento, elegir uno bueno, asegurarse de que ningún otro pasajero recibe al mismo conductor y después seguir un viaje que termina con un pago. Es una pregunta favorita en las entrevistas de diseño de sistemas porque mezcla un stream en tiempo real de gran volumen con un núcleo transaccional pequeño y estricto.
Este artículo diseña el backend paso a paso: requisitos y cifras de capacidad, ingesta de ubicaciones, indexación geoespacial con geohash, S2 y H3, emparejamiento y despacho, la máquina de estados del viaje y los fallos que hay que prever, con un diagrama de arquitectura que puedes copiar.
Requisitos
Funcionales:
- Los conductores se conectan y envían su ubicación de forma continua.
- Los pasajeros piden un viaje y ven los coches cercanos y un tiempo estimado de recogida.
- El sistema empareja a un pasajero con un conductor, que puede aceptar o rechazar.
- Ambas apps siguen el viaje en vivo; el viaje termina con una tarifa y un pago.
No funcionales:
- Un emparejamiento en pocos segundos.
- Ubicaciones de conductores con, como mucho, unos segundos de antigüedad.
- No asignar nunca un conductor a dos pasajeros.
- Los problemas de una ciudad no deben tumbar a las demás.
Capacidad: las cifras que dan forma al diseño
Supón, para un despliegue grande, un millón de conductores conectados en el pico, cada uno enviando una ubicación cada 4 segundos, y 20 000 solicitudes de viaje por minuto:
| Magnitud | Cálculo | Resultado |
|---|---|---|
| Actualizaciones de ubicación | 1 000 000 / 4 s | 250 000 por segundo |
| Ancho de banda de ingesta | 250 000 × ~100 bytes | ~25 MB/s |
| Historial bruto al ritmo pico | 25 MB/s × 86 400 s | ~2 TB al día |
| Solicitudes de viaje | 20 000 / 60 s | ~333 por segundo |
Aparecen dos cargas de trabajo muy distintas. Las actualizaciones de ubicación son enormes en volumen, pero cada una es pequeña, desechable y queda superada 4 segundos después. Las solicitudes de viaje son tres órdenes de magnitud menos frecuentes, pero cada una debe procesarse exactamente una vez y está ligada a dinero. La arquitectura las separa.
Ingesta de ubicaciones: guarda solo lo actual
Las apps de los conductores envían ubicaciones por una conexión persistente, como un WebSocket, o como pequeñas peticiones HTTPS periódicas. Un gateway de ubicaciones autentica al conductor, descarta puntos imposibles (un salto de 50 km en 4 segundos) y escribe cada actualización en un stream como Kafka, particionado por ciudad.
Tres consumidores leen el stream:
- El índice geoespacial en vivo guarda solo la última posición de cada conductor disponible, con una caducidad corta para que un conductor cuyo teléfono deja de responder desaparezca tras, digamos, 30 segundos.
- El seguimiento del viaje reenvía la posición del conductor asignado a la app del pasajero.
- El almacenamiento en frío guarda el historial completo en almacenamiento de objetos para analítica, modelos de ETA y resolución de disputas.
La decisión clave es que el índice caliente tiene una fila por conductor, no una fila por actualización. Un millón de conductores a unos cien bytes cada uno son unos 100 MB, lo bastante poco para mantenerlo en memoria por ciudad.
Indexación geoespacial
La pregunta del emparejamiento es «¿qué conductores disponibles están cerca de este punto?». Un índice de base de datos normal sobre latitud y longitud la responde mal, así que los sistemas asignan las coordenadas a celdas.
| Índice | Forma de la celda | Puntos fuertes | Cuidado con |
|---|---|---|---|
| Geohash | Rectángulos, codificados como prefijo de una cadena | Sencillo; los prefijos se anidan; Redis GEO lo usa | Las celdas varían de forma; las vecinas pueden tener prefijos distintos |
| S2 | Cuadriláteros sobre un cubo proyectado | Jerárquico, usado por Google | Más complejo de razonar |
| H3 | Hexágonos | Todas las vecinas están a la misma distancia; 16 resoluciones | Los hexágonos no se anidan perfectamente |
H3, que Uber publicó como código abierto en 2018, hace uniforme la búsqueda por radio: toma la celda del pasajero y el anillo de celdas a su alrededor, y después reúne a los conductores que hay dentro. La resolución importa. Según la tabla de resoluciones de H3:
| Resolución H3 | Área media de la celda | Longitud media de la arista |
|---|---|---|
| 7 | 5,16 km² | 1,41 km |
| 8 | 0,74 km² | 0,53 km |
| 9 | 0,11 km² | 0,20 km |
La resolución 8 o 9 va bien para ciudades densas; las zonas rurales necesitan anillos más amplios o celdas más gruesas.
Si quieres empezar con algo sencillo, Redis trae comandos geoespaciales integrados:
GEOADD drivers:sf -122.4194 37.7749 driver:8812
GEOSEARCH drivers:sf FROMLONLAT -122.4100 37.7800 BYRADIUS 2 km ASC COUNT 20 WITHDIST
GEOSEARCH, disponible desde Redis 6.2, devuelve los 20 conductores más cercanos en un radio de 2 km, ordenados por distancia. Haz sharding primero por ciudad, la frontera natural, y divide los lugares muy concurridos, como aeropuertos o estadios, en celdas más finas.
Emparejamiento y despacho
La distancia en línea recta es un mal indicador del tiempo de recogida: un conductor a 500 m al otro lado de un río puede estar a 15 minutos. Por eso el emparejamiento funciona en dos etapas:
- Generación de candidatos. Consulta el índice geo para obtener los conductores disponibles cerca del punto de recogida, filtrados por tipo de vehículo y otras restricciones.
- Ranking. Pide a un servicio de ETA los tiempos de recogida por la red de carreteras para los mejores candidatos y ordénalos por ETA, más factores como la dirección del conductor.
Emparejar cada solicitud al instante con el conductor más cercano es un enfoque voraz. Acumular solicitudes durante un segundo o dos y resolverlas juntas como un problema de asignación minimiza el tiempo total de espera de los pasajeros. El compromiso es un breve retraso a cambio de mejores emparejamientos en las horas de más demanda.
Después, el despacho envía una oferta a un solo conductor cada vez, con un tiempo límite de unos 15 segundos. Si la rechaza o se agota el tiempo, la oferta pasa al siguiente candidato.
No asignes nunca un conductor dos veces
Dos workers de emparejamiento pueden elegir al mismo conductor para dos pasajeros en el mismo segundo. Evítalo en el estado del conductor, no en la lógica de emparejamiento:
- Guarda el estado de cada conductor (
available,offered,on_trip) con un número de versión. - Una oferta es una actualización condicional: pon
offeredsolo si el estado sigue siendoavailableen la versión que leíste. - Exactamente un worker gana; el otro pasa a su siguiente candidato.
- Una oferta caducada devuelve al conductor a
available, de nuevo de forma condicional.
La misma idea de compare-and-set protege el lado del pasajero: una sola solicitud activa por pasajero.
La máquina de estados del viaje
Un viaje pasa por requested, matching, driver_assigned, driver_arrived, in_progress, completed y paid, con cancelación posible desde varios estados. El servicio de viajes guarda cada viaje en una base de datos relacional como Postgres, aplica las transiciones solo cuando el estado actual las permite y publica cada cambio como un evento para facturación, notificaciones, recibos y analítica.
Haz que cada transición sea idempotente, con el ID del viaje y la versión del estado como clave. Las redes móviles reintentan, y un «conductor llegó» entregado dos veces no debe hacer nada dos veces. El ejemplo de diagrama de estados muestra cómo dibujar este ciclo de vida por separado.
Actualizaciones en vivo, ETA y precios
Mientras un viaje está activo, la app del pasajero recibe la posición del conductor por una conexión push, limitada a cada pocos segundos y suavizada en el cliente. El mismo stream de ubicaciones alimenta el servicio de ETA, que necesita el tráfico en vivo, y los precios dinámicos, que comparan por celda las solicitudes con los conductores disponibles en una ventana deslizante. El pago se hace después del viaje a través de un proveedor de pagos; el diagrama de integración de una pasarela de pagos cubre esa parte.
Fallos que hay que prever
- Los conductores pierden la conexión. La app envía solo el último punto al reconectarse, y el índice da por caducados a los conductores que dejan de responder.
- Un worker de emparejamiento falla a mitad de una oferta. Las ofertas tienen hora de caducidad; el conductor vuelve a
availablecuando la oferta vence. - Falla el clúster de una ciudad. Como todo está particionado por ciudad, el fallo se queda en lo local. Los viajes en curso conservan su estado en el almacén duradero de viajes.
- GPS falsificado. Las comprobaciones de verosimilitud de velocidad y ruta en el gateway detectan los casos burdos.
El diagrama de la app de transporte
Contenido del 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
Cómo leerlo:
- El marco es una ciudad, la frontera de partición de todo lo que contiene.
- El camino de las ubicaciones va por arriba: app del conductor, gateway de ubicaciones, Kafka y, desde ahí, el índice geo de Redis y el almacén de objetos para el historial.
- El camino de las solicitudes va por debajo: la app del pasajero llama al emparejamiento, que lee los candidatos del índice geo, pide al servicio de ETA los tiempos de recogida y envía la oferta al conductor.
- El servicio de viajes es dueño de la máquina de estados y la guarda en Postgres.
- El bloque Markdown muestra las cifras de capacidad.
Dibujar diagramas de diseño de sistemas en tiempo real
- Separa el camino caliente del transaccional. Aquí, el stream de ubicaciones y el ciclo de vida del viaje tienen volúmenes, garantías y modos de fallo distintos; dibújalos como carriles separados.
- Escribe la clave de partición en el diagrama. «Particionado por ciudad» explica el escalado y el aislamiento de fallos en tres palabras.
- Muestra qué es actual y qué es historial. El índice de últimas posiciones y el almacén del historial responden a preguntas distintas.
- Etiqueta las aristas con tasas: 250 000 actualizaciones por segundo en una flecha dicen a los revisores dónde está la carga.
Para un enfoque general de estos diagramas, consulta el ejemplo de diagrama de diseño de sistemas. Las conexiones push se tratan en el diagrama de arquitectura de una app de chat, y el stream en sí en el diagrama de arquitectura de Kafka.
Constrúyelo en Archboard
- Inicia sesión con Google o GitHub y crea un diagrama.
- Añade un Marco del pack Básico con el nombre de la ciudad.
- Añade dos nodos Dispositivo del pack Red para la app del conductor y la app del pasajero.
- Añade una Puerta de enlace de API del pack Nube para el gateway de ubicaciones, un nodo Kafka del pack Datos para el stream, un nodo Redis para el índice geo y un Almacén de objetos para el historial.
- Añade tres nodos Instancia de cómputo del pack Nube para los servicios de emparejamiento, ETA y viajes, y un nodo Postgres del pack Datos para los viajes.
- Conecta el camino de las ubicaciones por arriba y el de las solicitudes por debajo, y etiqueta las aristas con lo que transportan y con qué frecuencia.
- Añade un bloque Markdown del pack Básico con las cifras de capacidad.
El ejemplo usa 12 nodos de diagrama, dentro del plan Free.
Preguntas frecuentes
¿Con qué frecuencia deben enviar los conductores su ubicación? Cada pocos segundos es un rango habitual. Las actualizaciones más frecuentes dan posiciones más frescas, pero multiplican el volumen de ingesta: reducir el intervalo a la mitad, de 4 a 2 segundos, duplica el tráfico.
¿Debo usar geohash, S2 o H3? Cualquiera funciona. Geohash es el más sencillo y viene integrado en Redis; H3 da distancias uniformes entre vecinas, lo que hace más limpias las búsquedas por radio y los precios por celda. Elige uno y úsalo de forma coherente.
¿Cómo se evita asignar un conductor a dos pasajeros? Haz que la oferta sea una actualización condicional sobre el estado del conductor, de modo que solo tenga éxito si el conductor sigue disponible en la versión que leyó el emparejador. Solo un emparejador puede ganar.
¿Por qué no emparejar a cada pasajero de inmediato con el conductor más cercano? El emparejamiento voraz puede dejar a los pasajeros posteriores con esperas largas. Agrupar las solicitudes durante un segundo o dos y resolverlas juntas reduce el tiempo total de espera, a costa de un breve retraso.
¿Qué base de datos guarda los viajes? Una base de datos relacional encaja bien: los viajes son transaccionales, necesitan transiciones de estado estrictas y alimentan la facturación. Los datos de ubicación de gran volumen viven en otro sitio, en memoria y en almacenamiento de objetos.