Transacciones distribuidas en microservicios REST: ¿mito o realidad?

Intentar transacciones ACID en microservicios REST es un dolor de cabeza. Te explico por qué no funcionan y qué alternativas reales usamos en la cancha.

Hace unos años, en un proyecto, me tocó lidiar con una creación de usuario que, aparte del registro básico, debía inicializar un par de cosas más en servicios distintos. Yo, pensando que podía amarrar todo con una gran transacción distribuida, terminé en un enredo que me costó varias noches. Cuando una parte fallaba, el sistema quedaba en un estado inconsistente y el rollback no era tan simple como esperaba. Ahí entendí de verdad por qué esto no escala.

El problema con las transacciones distribuidas y REST

Muchos caen en la tentación de replicar el modelo de transacción de base de datos en una arquitectura de microservicios. Quieres que, si creas un usuario y su billetera, todo sea atómico. Si algo falla, que se deshaga todo. Pero, ojo que los servicios REST son, por definición, stateless. No están diseñados para participar en un límite transaccional que abarque más de un servicio.

Implementar cosas como el Two-Phase Commit (2PC) a través de microservicios es, en mi experiencia, una receta para el desastre. En teoría suena bien, pero en la práctica es increíblemente complejo de mantener. Requiere mecanismos de recuperación robustos, reintentos y almacenamiento de estado. Incluso así, no es a prueba de fallos y puedes terminar con inconsistencias que debes resolver manualmente. De hecho, me frustra un poco la sobreingeniería que a veces vemos con esto.

Sagas y consistencia eventual: la ruta que sí funciona

Una alternativa que aparece son las Sagas o transacciones compensatorias. La idea es que si una operación falla, ejecutas una serie de operaciones para “deshacer” lo que ya se hizo. Es más manejable, pero igual tiene un overhead de implementación importante y tampoco es infalible. Siempre existe el riesgo de dejar el sistema en un estado temporalmente inconsistente.

Lo que me ha funcionado mejor, y lo que prefiero en casi todos los escenarios de microservicios, es la consistencia eventual. Es una solución más robusta y real. Cuando Bob se registra en tu sitio, la API Gateway no espera a que todo esté listo. Simplemente envía un mensaje a una cola y le responde a Bob: "Tu cuenta está en proceso, recibirás un correo de confirmación".

Un servicio consumidor escucha esa cola, procesa el mensaje y realiza los cambios en su base de datos, todo en una sola transacción local. Luego, si es necesario, envía otro evento o notifica a Bob. Esto desacopla los servicios y mejora el rendimiento. Además, te obliga a pensar en cómo tus servicios pueden ser idempotentes, lo que siempre es una buena práctica.

Un mensaje simple en la cola podría verse así:

{
  "eventType": "UserRegistered",
  "userId": "bob123",
  "email": "bob@example.com",
  "initialWalletAmount": 0
}

El servicio de usuario procesa esto para crear el usuario, y luego el servicio de billetera lo procesa para crear la billetera inicial, ambos de forma independiente pero coordinada por los eventos. Si la creación de la billetera falla, solo afecta a ese paso y se puede reintentar o compensar sin bloquear el registro principal.

Mi consejo si empezara de cero

Si tuviera que empezar de cero un proyecto con microservicios y sé que necesitaré coordinar acciones entre ellos, iría directo por un modelo de consistencia eventual con colas de mensajes. No perdería tiempo intentando forzar transacciones distribuidas al estilo ACID. Desde el día uno, diseñaría mis servicios para ser idempotentes y para que se comuniquen a través de eventos asíncronos. Es más simple, robusto y te ahorra muchos dolores de cabeza a futuro, créeme.

Jorge RequenaDeveloper full-stack · Chile