Un timeout informa que una respuesta no llegó a tiempo. No demuestra que la operación nunca comenzó ni que su efecto fue revertido. Una compra puede haberse registrado antes de que se cierre la conexión; repetirla con otra identidad puede crear una segunda compra.
La idempotencia permite repetir una operación conservando su efecto previsto dentro de un contrato. No exige ejecutar el handler una sola vez ni devolver siempre el mismo código HTTP. El problema central es reconocer el mismo intento y coordinar qué resultado está confirmado, pendiente o todavía desconocido.
Definir qué se repite
En una API de órdenes, una identidad puede combinar actor autenticado, tipo de operación y Idempotency-Key. La clave identifica el intento, no sustituye autorización ni identifica por sí sola a un usuario fiable. El contrato de API Design sigue validando permisos y entradas en cada petición.
Se conserva una huella de los parámetros aceptados. Su canonicalización debe estar definida: campos relevantes, moneda, cantidades y versión del contrato, sin depender accidentalmente del orden de propiedades JSON. Reutilizar una clave con una representación incompatible produce conflicto, no modifica silenciosamente el primer intento. Una nueva clave también puede violar una regla de negocio: una sola orden por reserva necesita su restricción independiente.
Efecto y registro en una frontera local
Para un efecto que vive en una base autoritativa, el registro de deduplicación y el cambio de negocio se confirman en la misma transacción. Una consulta previa que no encuentra la clave no basta: dos solicitudes concurrentes pueden observar su ausencia.
| Dato durable | Propósito |
|---|---|
| Actor, operación y clave | Restricción única, con campos no nulos, para reconocer un intento. |
| Huella de parámetros | Detectar reutilización incompatible. |
| Estado y resultado | Distinguir pendiente, completado y fallo definitivo. |
| Identidad de negocio | Aplicar invariantes que deben sobrevivir a nuevas claves. |
En PostgreSQL, insertar bajo una restricción única coordina la carrera. La aplicación debe resolver el conflicto, leer el estado confirmado y comparar la huella; ON CONFLICT no realiza esas decisiones automáticamente. Un competidor puede esperar por la transacción anterior. Deben acotarse esa espera y las transacciones, con recuperación de un resultado incierto.
Para el caso local, la creación del registro, el cambio y el resultado pueden pertenecer a una sola transacción corta. Si se necesita un proceso asíncrono, aceptar durablemente un estado pendiente es otro contrato; requiere recuperación posterior y no equivale a completar el efecto.
Un flujo con resultados observables
El núcleo del diagrama representa un efecto local. La ejecución concurrente se espera de forma acotada; consultar un pendiente asíncrono ya aceptado es un contrato distinto. Las llamadas a otro almacén o proveedor tienen otra frontera.
--- config: look: handDrawn --- flowchart TD client["Petición<br/>Actor + clave + parámetros"] --> identity["Validar identidad<br/>y huella del intento"] identity --> record subgraph local["Transacción"] record["Resolver registro único<br/>y huella del intento"] -->|"Nuevo"| effect["Registro + efecto + resultado<br/>Mismo commit local"] end effect --> result["Devolver resultado confirmado"] record -->|"Completado compatible"| result record -->|"Parámetros distintos"| conflict["Conflicto"] record -->|"Ejecución local en curso"| wait["Espera acotada<br/>Resolver commit o abort"] wait -->|"Resultado resuelto"| result record -->|"Pendiente asíncrono durable"| recovery["Consultar estado asíncrono<br/>o recuperar el intento"] recovery -->|"Resultado resuelto"| result
Una respuesta perdida después del commit permite devolver el resultado durable al reintentar. Si la transacción aborta, no queda un efecto confirmado separado de su registro. Esto protege esa frontera local, sin prometer ejecución única de código, notificaciones o servicios externos.
Efectos externos y resultado desconocido
Un cobro y una escritura en PostgreSQL no se vuelven atómicos por usar la misma clave. Se registra el intento, se coordina quién puede ejecutarlo y se conserva la identidad que reconoce el proveedor. La llamada de red queda fuera de una transacción larga, pero sacarla del bloqueo no resuelve su seguridad.
Si el proveedor acepta idempotencia, importan su alcance, huella y ventana de retención. Tras un timeout se consulta o reintenta el mismo intento bajo ese contrato. Fuera de la ventana, crear otro intento puede duplicar el cobro: se reconcilia primero. El sistema de boletaje conserva una orden y coordina sus intentos, incluidos resultados y notificaciones tardíos.
Cancelar localmente una tarea no prueba que el proveedor canceló su efecto. Los cambios de estado deben coordinar ejecución, cancelación y reconciliación para impedir que dos decisiones incompatibles se confirmen simultáneamente.
Propiedad temporal y retención
Un estado pendiente necesita una política de recuperación si su ejecutor desaparece. Una lease permite renovar o reclamar trabajo según un plazo, pero su vencimiento no detiene necesariamente al dueño anterior. Versiones de propietario y condiciones al confirmar pueden rechazar escrituras antiguas; un efecto externo necesita protección en su propia frontera.
La retención de las claves también limita la garantía. Si se elimina el resultado a las veinticuatro horas, un replay posterior puede parecer nuevo. Ese plazo es una decisión del ejemplo, no una recomendación universal. La identidad permanente de una orden, restricciones de negocio o un historial durable pueden ofrecer protección adicional. La limpieza considera privacidad, almacenamiento y plazo real de reintentos.
Consumidores y publicación
Los consumidores de Message Queues reconocen eventos por una identidad estable y coordinan deduplicación con su efecto local. Confirmar el mensaje después del commit admite otra entrega si se pierde el ack; el registro evita repetir ese cambio. Confirmarlo antes puede perder trabajo.
Una outbox conserva el evento con el cambio local, pero el relay puede publicarlo más de una vez. Orden y deduplicación son contratos diferentes: impedir duplicados no evita aplicar una versión antigua sobre otra nueva. La identidad del evento y la secuencia de la entidad resuelven preguntas distintas.
Comprobar el contrato
Las pruebas envían dos solicitudes iguales concurrentes, cambian el payload conservando la clave y pierden la respuesta después del commit. También detienen al ejecutor tras un efecto externo, dejan vencer su lease y entregan una respuesta tardía. El resultado esperado es una decisión durable explicable, no simplemente ausencia de errores HTTP.
Se observan conflictos de huella, intentos pendientes, antigüedad de resultados desconocidos, reconciliaciones y efectos redundantes. Una tasa alta de reintentos puede ser segura para el dato y costosa para la capacidad; limitar su presupuesto sigue siendo necesario. La corrección queda demostrada dentro de las fronteras y plazos declarados.
Referencias: semántica idempotente de HTTP, RFC 9110 y reintentos seguros con APIs idempotentes.