Arquitectura Impulsada por Eventos 2026: Por Qué los Microservicios se Están Pasando al Event Streaming

Hace unos meses estaba depurando un problema en producción que me tenía completamente perdido. Teníamos un servicio de órdenes que llamaba síncronamente a un servicio de inventario, que a su vez llamaba a un servicio de pagos, que llamaba a un servicio de notificaciones. Una cadena clásica de REST sobre REST. El problema: cuando el servicio de notificaciones se ponía lento (y se ponía lento bastante seguido), la latencia se propagaba hacia atrás como un dominó. Las órdenes empezaban a fallar. Los usuarios veían errores 500. Mi teléfono no paraba.

Ese día empecé a tomar en serio lo que varios colegas llevaban diciéndome desde 2024: la arquitectura de microservicios que todos construimos durante la década pasada tiene un problema estructural que los patrones de resiliencia tradicionales no resuelven del todo.

El Problema Real con los Microservicios Síncronos

Cuando digo “microservicios síncronos” me refiero a ese patrón donde cada servicio habla directamente con otros a través de HTTP o gRPC, esperando una respuesta antes de continuar. Es intuitivo. Es fácil de debuggear localmente. Y durante años funcionó razonablemente bien.

El problema es el acoplamiento temporal. Dos servicios que se comunican síncronamente están acoplados en el tiempo — los dos tienen que estar disponibles al mismo tiempo para que la operación funcione. Suena obvio cuando lo dices así, pero las consecuencias son más profundas de lo que parece.

En un sistema con 15 servicios donde cada llamada tiene 99.9% de disponibilidad, la disponibilidad compuesta de una cadena de 10 llamadas es aproximadamente 99%. Eso son unas 7 horas de downtime al mes. Empiezas a añadir circuit breakers, retries con backoff exponencial, timeouts en cascada… y de repente tienes un sistema complicado que sigue fallando, solo que de maneras más interesantes.

No estoy diciendo que los microservicios síncronos sean malos per se. Para ciertas operaciones — consultas de lectura, validaciones en tiempo real, operaciones que genuinamente necesitan una respuesta inmediata — siguen siendo la herramienta correcta. Lo que cambió en los últimos dos años es la forma en que los equipos piensan sobre qué operaciones realmente necesitan ser síncronas.

La respuesta, sorprendentemente, es que son muchas menos de las que creíamos.

Qué Cambió en 2025-2026

El event streaming no es nuevo. Kafka existe desde 2011. Lo que sí cambió es la madurez del ecosistema alrededor de él y, honestamente, el costo de operarlo.

Redpanda — que nunca dependió de ZooKeeper y usa Raft desde su origen, con compatibilidad casi perfecta con la API de Kafka — bajó significativamente la barrera de entrada. En mi equipo pasamos de gestionar un clúster de Kafka con 5 brokers más ZooKeeper a un clúster de Redpanda con 3 nodos. El consumo de memoria cayó a la mitad. No tengo que mentir: fue un alivio operacional real.

También maduró mucho el patrón Transactional Outbox. Antes, la pregunta “¿cómo garantizo que el evento se publique si la base de datos falla a mitad de la transacción?” no tenía una respuesta limpia. Ahora hay librerías sólidas — Debezium para CDC, Transactional Outbox con soporte nativo en varios ORMs — que hacen esto manejable.

Y por supuesto, Apache Kafka 4.0 salió en 2025 con el modo KRaft estable para producción, lo que simplificó bastante el modelo operacional para quienes no querían cambiar de Kafka.

Cómo Se Ve Esto en Código Real

Dejame mostrarte el patrón que adoptamos. Tenemos un servicio de órdenes que antes hacía esto:

# Antes: el servicio llama síncronamente a inventario, pagos, y notificaciones
async def create_order(order_data: OrderCreate) -> Order:
    # Si cualquiera de estos falla, la orden falla
    inventory_ok = await inventory_client.reserve(order_data.items)
    if not inventory_ok:
        raise HTTPException(status_code=409, detail="Stock insuficiente")

    payment_result = await payment_client.charge(
        user_id=order_data.user_id,
        amount=order_data.total
    )
    if not payment_result.success:
        # Ojo: aquí ya reservamos inventario y tenemos que revertir
        await inventory_client.release(order_data.items)
        raise HTTPException(status_code=402, detail="Pago rechazado")

    await notification_client.send_confirmation(order_data.user_id)

    order = await db.save(order_data)
    return order

El problema de esta versión, además del acoplamiento temporal, es la lógica de compensación manual. Si el pago falla tengo que revertir el inventario. Si la notificación falla… ¿qué hago? ¿Revierto todo? ¿Dejo la orden sin confirmación?

Ahora el mismo flujo con event streaming:

# Después: el servicio publica un evento y se desentiende del resto
async def create_order(order_data: OrderCreate) -> Order:
    async with db.transaction():
        # Guardamos la orden en estado PENDING
        order = await db.save(Order(
            **order_data.dict(),
            status=OrderStatus.PENDING
        ))

        # Usando el patrón Outbox: el evento se guarda en la misma transacción
        # Debezium o un worker lo publica a Kafka después
        await db.save(OutboxEvent(
            aggregate_id=order.id,
            event_type="OrderCreated",
            payload=order.to_event_dict(),
            created_at=datetime.utcnow()
        ))

    # Respuesta inmediata al cliente con el estado PENDING
    # Los servicios downstream reaccionan al evento de forma asíncrona
    return order

# En el servicio de inventario (consumidor independiente):
@kafka_consumer(topic="orders", event_type="OrderCreated")
async def handle_order_created(event: OrderCreatedEvent):
    result = await inventory.try_reserve(event.items)

    # Publica su propio evento según el resultado
    event_type = "InventoryReserved" if result.success else "InventoryFailed"
    await kafka.publish("inventory-events", {
        "order_id": event.order_id,
        "type": event_type,
        "reason": result.reason if not result.success else None
    })

La diferencia fundamental: el servicio de órdenes ya no sabe (ni le importa) si el inventario está disponible en ese momento. Publica un evento y termina. El servicio de inventario procesará el evento cuando pueda. Si está caído, lo procesará cuando se recupere — Kafka retiene los mensajes. La saga se ejecuta de forma asíncrona, cada servicio publicando eventos que otros consumen.

Esto no elimina la complejidad, ojo. La mueve. Ahora tienes que manejar la compensación distribuida (el patrón Saga), la eventual consistency, y los estados intermedios que el cliente puede ver. Pero es una complejidad más explícita y, en mi experiencia, más manejable a largo plazo.

El Gotcha que Me Costó Dos Días

Aquí va la parte que más me duele recordar: el event ordering.

Kafka garantiza el orden de los mensajes dentro de una partición. Lo que yo no consideré bien es que si particionas por order_id (que es lo natural), todos los eventos de la misma orden van a la misma partición. Perfecto. El problema vino cuando empezamos a particionar un topic por user_id para paralelizar el procesamiento.

Un usuario podía tener dos órdenes activas simultáneamente. Orden A se creó antes que Orden B, pero por un race condition en cómo el producer asignaba particiones, el evento OrderCreated de Orden B llegó antes al consumidor que el de Orden A. El servicio de inventario procesó Orden B primero, reservó los últimos 2 items del stock, y cuando llegó Orden A ya no había stock — aunque Orden A se había creado antes.

¿La solución? Para este caso específico necesitábamos que el orden relativo entre órdenes del mismo usuario se preservara. Pasamos a particionar el topic de órdenes también por user_id. Suena simple en retrospectiva, pero encontrar el bug me tomó dos días de logs y mucho café.

La lección general: no asumas que el orden de procesamiento refleja el orden de creación a menos que hayas diseñado explícitamente para eso. En sistemas event-driven el orden es algo que tienes que ganarte, no algo que viene gratis.

¿Cuándo Tiene Sentido y Cuándo No?

No voy a venderte que migres todo a eventos. Sería la respuesta fácil — y equivocada.

Los casos donde el event streaming realmente paga son:

  • Flujos de negocio largos y multi-paso donde la eventual consistency es aceptable (crear una orden, procesar pago, preparar envío — el usuario tolera esperar unos segundos para la confirmación)
  • Integración entre dominios donde los equipos son diferentes y no quieres que un dominio dependa del uptime de otro
  • Audit logs y event sourcing — tener el historial completo de eventos es valiosísimo para debugging y compliance
  • Fanout — un evento que múltiples servicios necesitan procesar (un UserRegistered que dispara onboarding, analytics, email de bienvenida, etc.)

Donde yo no lo usaría:

  • Operaciones que genuinamente necesitan respuesta síncrona (¿está este producto disponible en stock? — el usuario está esperando en la pantalla)
  • Sistemas pequeños con 2-3 servicios donde la complejidad operacional de Kafka no se justifica
  • Equipos sin experiencia en sistemas distribuidos que tienen deadline mañana — la curva de aprendizaje es real

Y honestamente, no estoy 100% seguro de que este patrón escale bien para todos los tamaños de equipo. Lo que funciona para un equipo de 8 personas con ownership claro de los topics puede volverse un caos de contratos de eventos sin versionar en una organización grande sin buena gobernanza.

Lo que Recomendaría Hoy

Si estás empezando un proyecto nuevo con más de 3-4 servicios, dedicaría tiempo desde el inicio a identificar qué flujos son candidatos para eventos. No migres síncronamente todo — empieza con los flujos donde la latencia asíncrona es aceptable y donde el acoplamiento entre servicios te está causando problemas reales.

Para la infraestructura: si puedes evitar operar Kafka tú mismo, hazlo. Confluent Cloud y Redpanda Cloud han madurado bastante. Si tienes que operarlo on-prem, Redpanda es mi recomendación actual sobre Kafka vanilla para equipos pequeños — menos overhead operacional es un factor real, y lo agradeces el domingo a las 2am cuando no hay una alerta de ZooKeeper.

Invierte en el Transactional Outbox desde el principio. No publiques eventos directamente desde la lógica de negocio sin garantías transaccionales — ese camino lleva a datos inconsistentes que son muy difíciles de debuggear.

Y aprende los patrones Saga (coreografía vs orquestación) antes de empezar. La coreografía — donde cada servicio reacciona a eventos y publica los suyos — es más desacoplada pero más difícil de razonar sobre el flujo completo. La orquestación — un servicio central que coordina la saga — es más visible pero introduce acoplamiento. Cuál usar depende de tu caso.

El problema del servicio de notificaciones lento que mencioné al inicio lo resolví, por cierto. Ahora el servicio de órdenes publica un evento OrderConfirmed y el servicio de notificaciones lo consume de forma independiente. Si se pone lento, los eventos se acumulan en el topic y los procesa cuando puede. Las órdenes no fallan. Mi teléfono descansa. Y yo, la verdad, también.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top