Skip to content

← All posts

August 16, 2026 · 4 min read

Por qué separamos la recepción del procesamiento con Kafka

  • kafka
  • arquitectura
  • eventos

This post is currently available in Spanish.

Cuando un cliente reporta un siniestro, ese momento es el peor posible para que el sistema falle. Y sin embargo, el diseño más común hace exactamente eso: encadena el reporte al procesamiento, de modo que si lo segundo se satura, lo primero deja de existir.

El problema de negocio (no el técnico)

Pensemos en un escenario común en el sector asegurador: los clientes necesitan reportar un siniestro desde donde estén, con una app móvil, un bot de mensajería, y los canales que vengan después.

El diseño de partida suele ser síncrono: el canal recibe el reporte y llama directamente al sistema que evalúa y despacha el caso. Funciona, hasta que no. Si el procesamiento está saturado o caído, el canal devuelve error. La persona que acaba de chocar, parada en la carretera con su teléfono, ve un "intenta más tarde".

Ahí está el error de diseño, y no es técnico. El reporte es una promesa: "tu siniestro quedó registrado, nosotros nos encargamos". El procesamiento es logística interna: evaluar, asignar, despachar. Mientras vivan acoplados, la promesa al cliente depende de que la logística esté de buenas. Y cada canal nuevo significa tocar el núcleo del sistema otra vez.

La solución que se planteó fue separar: el canal solo publica un evento en Kafka y confirma al cliente. Lo que pase después, pasa después.

Publicar no es lo difícil

Publicar el evento es una línea de código. Si el artículo terminara aquí, sería la documentación de Kafka con otro título.

Lo difícil es todo lo que pasa cuando el evento:

  • llega dos veces, porque el productor reintentó tras un timeout que en realidad sí había escrito, o porque un rebalanceo dejó offsets sin confirmar;
  • llega tarde, porque el consumidor estuvo caído veinte minutos y ahora procesa un backlog donde el mundo ya cambió;
  • no llega, porque alguien confundió acks=1 con durabilidad.

La garantía honesta con la que trabajas en la práctica es at least once: todo llega, pero puede llegar repetido. El modo exactly-once de Kafka existe y sirve dentro de Kafka, pero tu base de datos, tu servicio de notificaciones y el sistema del tercero que llamas no están dentro de esa transacción. El diseño real se hace para el evento repetido, no contra él.

Idempotencia en la práctica

La regla es una sola: procesar el mismo evento dos veces debe producir exactamente el mismo resultado que procesarlo una.

Para lograrlo necesitas dos cosas. Primero, una clave de idempotencia de negocio: un identificador del reporte generado en el canal en el momento de la captura. No sirve el offset ni un id técnico del mensaje: si el productor publica dos veces, son dos mensajes distintos con offsets distintos que representan el mismo siniestro.

Segundo, un candado persistente. La versión más simple y más robusta es una tabla de procesados con restricción de unicidad:

INSERT INTO reportes_procesados (clave_idempotencia, procesado_en)
VALUES (:clave, now());
-- Si viola la restricción de unicidad: ya lo procesaste. Confirma y sigue.

El detalle que separa un sistema correcto de uno que "casi siempre funciona": los efectos secundarios van detrás del mismo candado. Crear el caso, enviar la notificación, llamar al sistema externo. Si el candado dice "ya pasó", nada de eso se repite. Un reintento jamás debe generar dos siniestros, pero tampoco dos SMS al cliente asustado.

Reintentos y dead-letter queues

No todos los errores merecen reintento, y tratarlos igual es la receta para un consumidor atascado. Conviene separarlos en dos familias:

  • Transitorios (timeout de red, base de datos saturada, servicio externo en despliegue): reintento con backoff exponencial y jitter. El jitter no es adorno: sin él, todos los mensajes fallidos reintentan al mismo tiempo y vuelven a tumbar lo que apenas se estaba levantando.
  • Permanentes (payload que no cumple el esquema, regla de negocio que siempre va a rechazar): directo a la dead-letter queue. Reintentar cincuenta veces un JSON malformado no lo va a reparar; solo bloquea la partición para todos los mensajes de atrás.

Y la lección más cara de todas: la DLQ no es un basurero, es una bandeja de pendientes. Cada mensaje ahí es un cliente cuyo reporte no terminó de procesarse. Eso implica tres cosas que no vienen en ningún tutorial: una métrica con alerta cuando la DLQ crece, una persona dueña de revisarla, y un mecanismo probado para corregir y reinyectar. Si no existen las tres, no tienes una dead-letter queue: tienes un lugar elegante donde perder datos.

Lo que haría distinto

El patrón cumple lo que promete: el canal de recepción deja de depender del procesamiento, y agregar un canal nuevo pasa de "tocar el núcleo" a "escribir su capa de integración". Pero no es gratis, y hay cosas que hoy haría antes o no haría:

  • Versionar los esquemas de eventos desde el día uno. El primer cambio de contrato con consumidores viejos leyendo mensajes nuevos duele exactamente lo que estás imaginando.
  • Presupuestar la observabilidad como parte del patrón, no como mejora. Sin métricas de lag por partición y trazas de extremo a extremo, depurar un evento que "no llegó" es arqueología.
  • No usaría este patrón para flujos que exigen respuesta síncrona real (una autorización de pago no puede ser "luego te aviso"), ni en sistemas cuyo volumen no justifica operar Kafka. La cola no es un fin; es el precio de una promesa que decidiste sostener.

Esta arquitectura no hace al sistema más rápido. Lo hace más honesto: la promesa al cliente queda separada de la logística interna, y cada una puede fallar sin arrastrar a la otra. Eso, en seguros, es la diferencia entre un incidente y un cliente que no vuelve.