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 exists → insert, 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.
  • exists → insert: 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.