ADR-01.003 / ADR Decisión

ADR-01.003 / 2026-08-15

Consumo idempotente mediante identidad de la orden

El consumidor deduplica OrderReceived mediante order_id como clave primaria y llama a acknowledge sólo después de que materialize() retorna sin excepción.

Contexto

Kafka y el transactional outbox proporcionan entrega at-least-once, no exactamente una vez. Por ello, un mismo evento OrderReceived puede llegar duplicado, incluso después de que su primer procesamiento haya terminado.

Decisión

received_orders.order_id es la clave primaria de la tabla de recepción. El consumidor ejecuta la inserción con INSERT ... ON CONFLICT (order_id) DO NOTHING dentro de una transacción de base de datos. La PK y la cláusula ON CONFLICT son la garantía de deduplicación en la base de datos. No se usa el patrón existsinsert, porque una comprobación previa no resuelve la carrera entre consumidores.

OrderReceivedListener llama acknowledgment.acknowledge() sólo después de que materialize() retorna sin excepción; si falla, no llama a acknowledge(). Puede ocurrir redelivery, y ON CONFLICT la vuelve inocua sin duplicar received_orders. El consumo permanece at-least-once, no exactly-once.

Consecuencias

El consumo permanece at-least-once y requiere que orderId/order_id sea la identidad estable del mismo aggregate. No es la clave de idempotencia de negocio de recepción: esa función corresponde a externalReference. La garantía se limita al consumo de OrderReceived y a received_orders; no es un framework genérico de deduplicación. La solución depende de PostgreSQL como autoridad de unicidad y mantiene una transacción de base de datos por procesamiento.

Alternativas descartadas

  • Entrega exactamente una vez de extremo a extremo: se descarta porque Kafka no garantiza por sí solo el commit en la base de datos PostgreSQL.
  • existsinsert: se descarta porque conserva una condición de carrera y no sustituye la restricción durable de unicidad.
  • Tabla genérica processed_messages para este único caso: se descarta porque añade una abstracción que no aporta valor frente a la identidad estable del aggregate order_id.
  • Redis o locks distribuidos: se descartan porque añaden complejidad y no sustituyen la restricción durable de unicidad.