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_messagespara este único caso: se descarta porque añade una abstracción que no aporta valor frente a la identidad estable del aggregateorder_id. - Redis o locks distribuidos: se descartan porque añaden complejidad y no sustituyen la restricción durable de unicidad.