Arquitectura Impulsada por Eventos en 2026: Por Qué Dejé de Pelear con REST Entre Microservicios

Hace unos diez meses, un servicio de notificaciones llamó a un servicio de inventario, que llamó a un servicio de precios, que estaba caído por un deploy mal sincronizado. La cadena entera se fue al piso. Era viernes por la tarde, teníamos tres usuarios reportando errores raros en checkout, y yo estaba intentando trazar qué había llamado a qué revisando logs de cuatro servicios diferentes en paralelo.

Ese fue el momento en que decidí que ya no quería más arquitectura síncrona entre microservicios.

No porque REST sea malo — en el boundary entre frontend y backend sigue siendo perfectamente razonable. Sino porque usar REST para comunicación interna entre servicios es básicamente construir dependencias de tiempo de ejecución que no se ven hasta que algo explota en producción.

El Problema Real con Microservicios Síncronos (No Es Lo Que Crees)

Cuando empecé a investigar event streaming, asumí que el beneficio principal era rendimiento. Pensé: “okay, Kafka es rápido, mis requests van a ser más rápidos.” Estaba completamente equivocado sobre el por qué vale la pena.

El problema con los microservicios síncronos no es velocidad. Es acoplamiento temporal. Cuando el Servicio A hace un HTTP request al Servicio B, en ese momento exacto ambos servicios necesitan estar vivos, responder dentro del timeout, y estar de acuerdo sobre el contrato de la API. Si cualquiera de esas tres cosas falla, el error se propaga hacia arriba. Y en un sistema de 8-10 servicios (que es donde estaba mi equipo — somos cuatro ingenieros manejando una plataforma de e-commerce con servicios separados para inventario, órdenes, notificaciones, pagos, búsqueda y usuarios), las cadenas de dependencias se vuelven imposibles de razonar.

El circuit breaker pattern mitiga esto, pero no lo resuelve. Estaba usando Resilience4j en nuestros servicios de Spring Boot y aun así terminaba debuggeando cascadas de fallbacks que producían estados inconsistentes difíciles de reproducir.

Lo que event streaming resuelve es diferente: el productor publica un evento y no le importa quién lo consume ni cuándo. El consumidor procesa cuando puede. El desacoplamiento temporal es la feature, no un side effect.

Kafka vs Redpanda en 2026: Elegí Mal la Primera Vez

Empecé con Apache Kafka 3.8. Documentación sólida, ecosistema enorme, Confluent Schema Registry disponible. Pasé dos semanas configurando el cluster en nuestro entorno de staging — Kubernetes en AWS, tres brokers, ZooKeeper reemplazado por KRaft (que ya es el default desde 3.3, por si alguien todavía está en versiones viejas).

El setup funcionó. Los benchmarks de throughput eran buenos. Pero gestionar Kafka en un equipo de cuatro personas donde nadie tiene experiencia profunda de operaciones de Kafka es… complicado. El tuning de configuración — num.io.threads, log.segment.bytes, replica.fetch.max.bytes — requiere entender mucho contexto que simplemente no tenía.

Cambié a Redpanda 24.3 después de un post de un ingeniero de Cloudflare que describía exactamente mi situación. Y honestamente, fue la decisión correcta para nuestro tamaño de equipo. Redpanda es compatible con la API de Kafka al 100% — mis producers y consumers no cambiaron ni una línea — pero operacionalmente es mucho más simple. No tiene ZooKeeper, no tiene JVM, el binario solo hace una cosa y la hace bien. El lag en nuestro entorno bajó de ~15ms a ~3ms para el percentil 99, aunque admito que probablemente eso es más sobre configuración que sobre la herramienta en sí.

Si trabajas en una empresa grande con un equipo de plataforma dedicado, Kafka sigue siendo la respuesta correcta. El ecosistema de Confluent, los conectores, el soporte enterprise — todo eso vale la pena si tienes la operación para manejarlo. Para equipos pequeños sin ese lujo, Redpanda les va a quitar mucho dolor de cabeza.

La Implementación Que Realmente Uso en Producción

El patrón que terminé adoptando es bastante estándar: transactional outbox para garantizar que los eventos se publiquen consistentemente con los cambios en base de datos, y consumers con procesamiento idempotente.

Acá está la versión simplificada de cómo publico eventos desde el servicio de órdenes (usamos Node.js con TypeScript):

// order-service/src/events/order-event-publisher.ts
import { Kafka, Producer, RecordMetadata } from 'kafkajs';

// Nota: usamos kafkajs 2.2.4 — la 2.3.x tenía un bug con reconnects
// en conexiones de larga duración, ver issue #2031 en su GitHub
const kafka = new Kafka({
  clientId: 'order-service',
  brokers: process.env.KAFKA_BROKERS?.split(',') ?? ['localhost:9092'],
  retry: {
    initialRetryTime: 100,
    retries: 8
  }
});

interface OrderCreatedEvent {
  orderId: string;
  userId: string;
  items: Array<{ productId: string; quantity: number; price: number }>;
  totalAmount: number;
  createdAt: string; // ISO 8601
}

export class OrderEventPublisher {
  private producer: Producer;
  private connected = false;

  constructor() {
    this.producer = kafka.producer({
      // Esto es importante: sin esto, puedes perder mensajes
      // si el broker tiene un failover justo cuando publicas
      allowAutoTopicCreation: false,
      transactionTimeout: 30000,
    });
  }

  async publish(event: OrderCreatedEvent): Promise<RecordMetadata[]> {
    if (!this.connected) {
      await this.producer.connect();
      this.connected = true;
    }

    return this.producer.send({
      topic: 'orders.created.v2',
      messages: [{
        key: event.orderId,  // partitioning por orderId — crucial para ordering
        value: JSON.stringify(event),
        headers: {
          'event-type': 'OrderCreated',
          'schema-version': '2',
          'source-service': 'order-service',
        }
      }]
    });
  }
}

Y del lado del consumidor, en el servicio de notificaciones:

// notification-service/src/consumers/order-consumer.ts
import { Kafka, Consumer, EachMessagePayload } from 'kafkajs';

const kafka = new Kafka({
  clientId: 'notification-service',
  brokers: process.env.KAFKA_BROKERS?.split(',') ?? ['localhost:9092'],
});

export class OrderEventConsumer {
  private consumer: Consumer;

  constructor(private readonly notificationService: NotificationService) {
    this.consumer = kafka.consumer({
      groupId: 'notification-service-order-group',
      // sessionTimeout más alto que el default (10s) porque nuestro
      // procesamiento de notificaciones puede tardar hasta 8 segundos
      sessionTimeout: 30000,
    });
  }

  async start(): Promise<void> {
    await this.consumer.connect();
    await this.consumer.subscribe({
      topic: 'orders.created.v2',
      fromBeginning: false
    });

    await this.consumer.run({
      // autoCommit: false es non-negotiable si quieres exactly-once semántica
      autoCommit: false,
      eachMessage: async ({ topic, partition, message, heartbeat }: EachMessagePayload) => {
        const eventData = JSON.parse(message.value?.toString() ?? '{}');

        // Chequeo de idempotencia antes de procesar
        const alreadyProcessed = await this.notificationService
          .isEventProcessed(message.offset, partition);

        if (!alreadyProcessed) {
          await this.notificationService.sendOrderConfirmation(eventData);
          await this.notificationService.markEventProcessed(message.offset, partition);
        }

        // Commit manual después de procesar exitosamente
        await this.consumer.commitOffsets([{
          topic,
          partition,
          offset: (BigInt(message.offset) + 1n).toString()
        }]);
      }
    });
  }
}

Un detalle que me costó un fin de semana entender: el heartbeat que viene en eachMessage — si tu procesamiento tarda más que sessionTimeout / 2, el broker va a asumir que el consumer murió y va a reasignar la partición. En mi caso, una integración con un proveedor de emails externo que a veces tarda 7-8 segundos casi me destruyó en producción. Tuve que llamar await heartbeat() dentro del procesamiento largo para mantener la sesión viva.

La Parte Que Nadie Menciona: Evolución de Schemas

Aquí es donde me quemé más. Y donde la mayoría de los posts de blog que leí eran demasiado optimistas.

El problema: publiqué el topic orders.created.v1 con una estructura. Tres semanas después, necesitaba agregar un campo. Pensé: “fácil, solo agrego el campo, es backwards compatible.” Y sí, técnicamente lo es — mis consumers existentes ignoraban el campo nuevo.

Hasta que un consumer más viejo que había estado procesando mensajes pausado por un par de horas reinició desde su offset guardado, procesó 400 mensajes del formato nuevo, y tiró excepciones porque no manejaba el campo nuevo correctamente. Esto pasó a las 2am de un martes y me enteré por las alertas de Datadog a las 8am.

El fix correcto — que ahora implementé — es Avro con Confluent Schema Registry. El registry te fuerza a registrar schemas y valida compatibilidad antes de permitir publicaciones. Pero tiene un costo operacional. Para equipos chicos sin la infraestructura de Confluent, lo mínimo que recomendaría es:

  1. Versionar los topics (orders.created.v1, orders.created.v2) en lugar de evolucionar el schema in-place
  2. Incluir siempre un header schema-version (como en mi código arriba)
  3. Mantener consumers con handling explícito por versión durante el período de transición

No es tan elegante como el registry, pero es mucho más simple de operar. Y evita el tipo de sorpresa que me dio ese martes.

Dicho esto, si tu organización ya usa Confluent Platform, el Schema Registry vale la pena desde el día uno. No seas como yo y lo agregues después de tener un incidente.

Qué Patrones Realmente Funcionan y Cuáles Son Over-Engineering

Después de unos meses en producción, tengo opiniones más definidas sobre qué usar y qué no.

Event Sourcing completo: probablemente no lo necesitas. Es uno de esos patterns que lees en los blogs de arquitectura y parece que deberías usar siempre. En mi experiencia con un equipo chico, la complejidad de mantener un event log como fuente de verdad y reconstruir estado a partir de events históricos crea más problemas de los que resuelve, a menos que tengas un requerimiento muy específico de auditoría o replay. Lo estuve considerando para el servicio de inventario y decidí no hacerlo. No me arrepentí.

CQRS combinado con event streaming: esto sí funciona bien si tienes casos de uso de lectura y escritura con requisitos muy diferentes. Usamos una versión simplificada: los writes van a nuestra base de datos transaccional (PostgreSQL), y los events que publicamos mantienen sincronizado un índice de Elasticsearch para búsqueda. No es CQRS puro, pero resuelve el problema real.

Saga pattern para transacciones distribuidas: necesario si tienes operaciones que cruzan múltiples servicios y necesitas rollback coordinado. Implementé esto para el flow de checkout — si el pago falla, el inventario reservado tiene que liberarse. La implementación con compensating transactions y un saga orchestrator (usamos Temporal para esto, que tiene muy buena integración con Kafka) funciona bien, pero agregas complejidad operacional. Mi recomendación: si puedes diseñar el sistema para que las operaciones sean idempotentes y eventualmente consistentes sin necesitar rollback síncrono, hazlo. El saga pattern es el último recurso, no el primero.

Lo Que Haría Distinto Si Empezara de Nuevo

¿Qué recomendaría concretamente? Si estás en un equipo de menos de 8 ingenieros y no tienes un equipo de plataforma dedicado, empieza con Redpanda en lugar de Kafka — la paridad de API es real y te va a ahorrar semanas de tuning operacional.

Usa topics versionados desde el día uno, no intentes evolucionar schemas in-place. Implementa idempotencia en tus consumers antes de preocuparte por cualquier otra cosa — es la garantía que más necesitas en la práctica. Y no migres todo de golpe: identifica los flujos donde el desacoplamiento temporal te da más valor (generalmente los que tienen más dependencias síncronas en cadena) y empieza por ahí.

Yo no estoy 100% seguro de que este approach escale más allá de 20-30 servicios sin revisitar algunas decisiones, pero para el tamaño donde estamos ahora, es lo más sensato que hemos hecho en arquitectura en dos años.

Leave a Comment

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

Scroll to Top