Volver a case studies

Arquitectura orientada a eventos con NATS

Desacoplar el feed social de la operación de torneos en vivo — y ser honesto sobre qué está realmente conectado versus solo declarado.

NATS JetStream
Arquitectura orientada a eventos
Backend
Problem

El feed social (posts, follows, actividad de jugador) es un servicio separado (social-bff) del backend principal de torneos, y no debería poder ralentizar o romper el registro de puntajes, las inscripciones, o la automatización de torneos — pero aun así necesita saber cuándo termina un partido o se inscribe un jugador, de forma confiable, no solo "casi siempre".

Solution

Una llamada HTTP directa del backend a social-bff en cada acción relevante acoplaría la disponibilidad de ambos servicios, así que usé NATS JetStream en su lugar — elegido específicamente sobre NATS simple porque social-bff necesita entrega at-least-once con replay, algo que el pub/sub simple no da. Un único stream de JetStream (UFT_EVENTS), convención de subject uft.<dominio>.<evento>, retención de 7 días. backend-nest publica fire-and-forget: cada llamada de publicación está envuelta de forma que si NATS no es alcanzable, se registra en logs y se ignora — una caída de NATS nunca debe romper el request que la originó. social-bff consume a través de un consumer durable y nombrado de JetStream con ack/nak explícito, así retoma exactamente donde quedó tras un reinicio.

Trade-offs

Se declaran cuatro subjects de evento, pero solo dos se publican realmente en algún lugar del código hoy. Los otros dos se consumen del lado de social-bff pero no tienen todavía un punto de publicación en backend-nest — evidencia real de diseñar el contrato antes que la implementación, no un vacío oculto. Por separado, las constantes de subject se duplican a mano entre ambos servicios (no hay paquete compartido), con un comentario que exige sincronización manual — un riesgo de acoplamiento real que una librería compartida eliminaría.

Lessons learned

En producción, terminar un partido publica uft.match.finished con ambos equipos, puntajes y categoría, y social-bff lo convierte en un post de feed autorado por el sistema. Confirmar una inscripción publica uft.player.registered_tournament una vez por cada jugador del roster, y cada uno se convierte en un post de actividad personal autorado por la cuenta de ese jugador, no un post genérico del sistema. Publicación fire-and-forget combinada con un consumer durable y con ack explícito es una buena división para "el producto principal nunca debe romperse porque una funcionalidad secundaria está caída" — pero declarar más del contrato de eventos de lo que realmente está conectado solo es honesto si se hace seguimiento de qué mitad es real, que es por eso que este texto lo dice directamente. Las constantes sincronizadas a mano son la siguiente deuda que vale la pena eliminar, idealmente antes de que un tercer consumidor empeore el problema.