Marketplace náutico
Alquiler de motos de agua del anuncio al pago: tres aplicaciones sobre un único backend, con el camino del dinero tratado como la parte que no admite ensayo.
GoJet acerca a quien tiene una moto de agua parada y a quien quiere el día en el agua, y absorbe la parte incómoda: reserva, pago, fianza, identidad verificada y valoración por ambos lados. Son tres aplicaciones, web, iPhone y Android, sobre un único backend en Java, dueño de los datos y de los contratos. Antes de darlo por terminado, ejecutamos el recorrido entero contra la API real: 22 de 22 etapas, pago y devolución incluidos.
- 22 de 22Etapas del recorrido de negocio ejecutadas contra la API real
- 792Pruebas automáticas verdes en el backend, incluidas las de arquitectura
- 225 de 225Pruebas de la app en la medición de la auditoría, en ejecución serial
- 14Dominios de negocio en el backend, de identidad a seguros
- 14 minutosDel push al backend en el aire: integración continua más cambio de imagen
Una moto de agua parada de un lado, un acuerdo a ciegas del otro
Quien tiene una moto de agua sabe que pasa más tiempo en el garaje que en el agua. Quien quiere alquilar una por un fin de semana casi siempre acaba en un grupo de WhatsApp, acuerda el precio en efectivo y confía en que salga bien. No hay contrato, no hay fianza, nadie comprueba quién está del otro lado, y la agenda vive en la cabeza del propietario, hasta el día en que dos personas reservan el mismo sábado.
Lo que falta en ese arreglo no es una aplicación, es confianza. Y la confianza, en un marketplace de dos lados, no es discurso: es orden de ejecución, restricción de base de datos y verificación de titularidad. Así tratamos el problema desde el primer día: la incomodidad se queda con la plataforma, no con dos personas que solo querían pasar el domingo en el agua.
El producto se diseñó a partir de tres personas: quien alquila, quien anuncia y el equipo que cuida el trasfondo, moderación, verificación de identidad, seguro y señales de fraude. Las dos primeras se encuentran en el momento de la reserva; la tercera nunca aparece en la pantalla del cliente, y justamente por eso tiene que existir.
La reserva solo nace cuando entra el dinero
El camino del dinero no admite ensayo. En una misma reserva hay reparto de valor entre plataforma y propietario, una fianza bloqueada en la tarjeta antes de la entrega y una devolución cuando la reserva se cae. Equivocarse aquí no genera un log feo, genera un cobro doble en la tarjeta de alguien, y eso no se deshace con un despliegue.
Elegimos Stripe Connect con cargo destinado y comisión de aplicación: la misma transacción sale ya repartida entre la plataforma y el propietario, en vez de entrar entera y devolverse después. La fianza es una autorización fuera de sesión sobre la tarjeta guardada como cliente del proveedor, y la cancelación revierte la transferencia original en lugar de emitir un pago nuevo en sentido contrario.
La confirmación de la reserva no viene del clic del usuario: viene del aviso del proveedor de pago, verificado por firma antes de cualquier procesamiento e idempotente por clave: un reenvío del mismo evento no confirma dos veces. El recorrido de pagar, recibir el aviso, ver la reserva confirmada y después devolver el dinero se ejecutó de punta a punta en producción, todavía en modo de prueba, el 2 de julio de 2026.
Un detalle pequeño que solo aparece bajo concurrencia: la fianza ganó bloqueo optimista de versión, y el registro se graba y se descarga en la base de datos antes de llamar a la pasarela, en una migración dedicada. Así, cuando dos solicitudes compiten por la misma fianza, quien pierde la carrera recibe un error de conflicto sin que se haya movido un céntimo. El orden de las operaciones es la garantía; el mensaje de error es solo la consecuencia.
La regla que la base de datos se niega a romper
Dos personas nunca reservan el mismo periodo de la misma moto de agua. Esa regla existe en el servicio que comprueba conflictos de fechas, como cabría esperar, pero existe también dentro de PostgreSQL, como restricción de exclusión sobre un intervalo de fechas con ambos extremos inclusivos, apoyada en la extensión btree_gist. Un camino de código que olvidara la comprobación sencillamente no lograría grabar la fila.
Esa elección tuvo un precio, y vale la pena contarlo: el H2 que ejecuta la suite rápida de pruebas no entiende esa sintaxis. La migración pasó entonces a un directorio hermano, versionado por fabricante de base de datos: hermano, y no subdirectorio, porque una carpeta dentro de la ruta por defecto entraría dos veces en el barrido recursivo de Flyway y la base se negaría a arrancar acusando versión duplicada.
El backend es hexagonal, organizado por módulo de negocio: son 14 dominios, de identidad y flota a pagos, seguros y fraude. Las fronteras entre ellos no dependen de la disciplina: las verifica una prueba de arquitectura que rompe la compilación a propósito cuando un módulo alcanza el interior de otro. El esquema evoluciona solo por migración numerada e inmutable; la próxima por escribir es la número 62, y una ya aplicada nunca se renumera, porque Flyway se negaría a arrancar por divergencia de checksum.
Una cuenta, dos papeles y la autorización que decide por el conjunto
La misma persona alquila un fin de semana y anuncia el siguiente. El perfil de propietario se activa cuando ella quiera, tras una verificación, sin segunda cuenta y sin segundo inicio de sesión. Es fácil de prometer en la pantalla y traicionero de sostener por debajo.
Lo aprendimos con un fallo nuestro. La autorización decidía por el papel activo, el que la interfaz usa solo para saber en qué modo está navegando la persona; el resultado era que un usuario con los dos papeles, en modo propietario, recibía un 403 en cada intento de reservar. La invariante pasó a ser: el acceso lo decide el conjunto de papeles de la sesión, mediante un único predicado, en los tres puntos de verificación: el middleware de rutas, el guardián de página y el reenvío al backend. El papel activo quedó restringido al aterrizaje y a las redirecciones.
Una instrucción escrita en un documento no es una barrera técnica, así que la regla ganó una guarda mecánica: una regla de lint rompe la verificación si la cookie de papel se lee fuera de los archivos con derecho a tocarla, y una prueba unitaria fija el comportamiento del predicado. En la misma línea, la verificación de identidad tiene un valor por defecto manual y a prueba de fallos, sin proveedor real configurado, quien pide convertirse en propietario queda pendiente hasta que un administrador apruebe. El valor anterior aprobaba solo; lo cambiamos, y fijamos la variable explícitamente en el entorno de producción.
Tres aplicaciones, un contrato
El backend es Java 21 con Spring Boot sobre PostgreSQL 16, y es el dueño de los datos y de los contratos. El sitio es Next.js actuando como BFF: el navegador nunca habla con el backend directamente, la sesión vive en una cookie httpOnly, convertida en el servidor en una cabecera de autorización, y el token jamás llega al almacenamiento local del navegador. La app, en Expo y React Native, habla directo con la API por token, sin intermediario.
Tres clientes sobre un único contrato tienen una forma conocida de desincronizarse: alguien cambia el contrato y lo cuenta después. Por eso todo cambio de contrato exige una decisión registrada por escrito en el repositorio del backend, antes del código. Es burocrático a propósito, y es lo que mantiene la regla de negocio con una sola fuente.
Algunos cuidados solo aparecen cuando el producto está en el aire. Cada llamada del BFF al backend lleva un tiempo límite de 8 segundos que se convierte en un error explícito de indisponibilidad, sin él, la función queda colgada hasta el límite de la plataforma y el usuario recibe un mensaje genérico que no ayuda a nadie. Y un código de error ambiguo nunca entra en el diccionario global: el backend responde no autorizado tanto para una contraseña incorrecta como para una sesión caducada, y traducirlo de una vez haría que la frase de inicio de sesión inválido se filtrara a unos 20 puntos de llamada que nada tienen que ver con el login.
Auditamos nuestro propio producto antes de venderlo
Antes de darlo por terminado, sometimos el ecosistema a una auditoría de recorridos contra una pila real, no a una lectura de código. Una ejecución completa recorrió 22 de 22 etapas por la API: el propietario crea desde cero y publica, el arrendatario reserva, paga, deja la fianza, concluye y valora, y un segundo ciclo cancela con devolución. En el backend la suite cierra con 792 pruebas verdes, incluidas las de arquitectura; en la app fueron 225 de 225 en la medición de la auditoría.
La parte que más nos enseñó fue la suite visual grabada: ocho vídeos y un recorrido continuo de 41 pasos en el que el arrendatario alquila exactamente la moto de agua que el propietario acaba de publicar. Uno de esos recorridos termina en rojo a propósito: los problemas se registran con aserción suave, así que el vídeo graba el ciclo entero y el informe acusa el defecto al final. Una suite de auditoría que esconde un defecto para quedar verde no sirve para auditar nada.
La auditoría encontró cosas de verdad, y la más grave estaba en el camino del dinero: un endpoint de checkout aceptaba el identificador del pedido sin comprobar la titularidad y además reenviaba la clave de idempotencia enviada por el cliente: una puerta para que un arrendatario pagara el pedido de otro y una ventana de cobro doble. Lo corregimos con verificación de titularidad, una respuesta 404 uniforme para no permitir enumerar identificadores y una clave de deduplicación derivada en el servidor. También aprendimos que sondear la salud del proceso no es sondear el dato: el backend responde saludable en unos 21 segundos, antes de que termine la carga de ejemplo, y el guion pasó a esperar a que la búsqueda devuelva resultados.
Dónde está hoy y qué falta para abrir las puertas
GoJet está en piloto de producción, no en maqueta. El backend corre en un VPS con Docker Compose y Caddy, con certificado propio; el sitio está en Vercel; y la regla medida entre un push y el código en el aire, en el backend, es de 14 minutos: unos diez de integración continua más cuatro hasta que el supervisor de contenedores cambia la imagen, medidos el 31 de julio de 2026. El proveedor de pago ya está desplegado en producción, todavía con claves de prueba.
Lo que falta es, en buena parte, comercial y de papeleo: activar las claves reales, cerrar el perfil de la plataforma con el proveedor y volver a pasar a los propietarios por su alta, publicar la app en las tiendas y someter los términos a revisión jurídica. Existe un runbook escrito para ese cambio, porque el momento de descubrir el paso olvidado no es con dinero real en el camino.
Y hay huecos que preferimos declarar antes que maquillar. El propietario que solo usa la app todavía no concluye por sí mismo la activación del cobro: el alta alojada del proveedor no está expuesta allí, y hoy el camino pasa por el sitio. Una reserva creada y concluida el mismo día sigue también fuera de alcance, porque la regla que impide una fecha de inicio en el pasado y la que exige que el alquiler haya terminado se anulan entre sí. Ambas están en la cola, con corrección conocida.
Un marketplace entero, del primer clic al pago en la cuenta del propietario, funcionando en producción con el camino del dinero tratado como camino del dinero: verificado etapa por etapa contra la API real, y con los huecos que quedan escritos en vez de escondidos.