¿Cómo diseñarías una API de pagos idempotente para que, si el cliente reintenta por un timeout, no se cobre dos veces?
$ interview --rol dev
Evalúa diseño de sistemas distribuidos: claves de idempotencia, manejo de concurrencia, persistencia atómica y comportamiento ante fallos parciales con proveedores externos.
En redes reales un timeout no dice si la operación se ejecutó o no, así que el cliente va a reintentar. La solución estándar es una clave de idempotencia.
Diseño:
El cliente genera un UUID por intención de pago y lo manda en un header, por ejemplo Idempotency-Key. Los reintentos reutilizan la misma clave.
El servidor guarda la clave con restricción UNIQUE (por cliente), un hash del body, el estado (en_proceso / completado / fallido) y la respuesta final.
Código: sql
INSERT INTO idempotencia (clave, cliente_id, hash_body, estado)
VALUES (:k, :c, :h, 'en_proceso');
-- si viola UNIQUE, es un reintento
Flujo:
Clave nueva: se inserta y se procesa el cobro.
Clave existente y completada: se devuelve la respuesta guardada, sin volver a cobrar.
Clave en proceso: se responde 409 o se espera; así se evita la carrera entre dos requests simultáneos.
Misma clave con body distinto: 422, es un error del cliente.
Puntos finos:
La inserción de la clave debe ser atómica (constraint en la base, no un SELECT y luego INSERT).
Si llamás a un proveedor externo, propagale también una clave de idempotencia; la mayoría de las pasarelas la soportan. Si no, necesitás conciliación posterior.
Para estados 'en_proceso' colgados por caída del servicio, un proceso de reconciliación consulta al proveedor y cierra el estado.
Definí retención de claves (por ejemplo 24 h o más) y documentalo.
Usá el patrón outbox para publicar eventos de 'pago realizado' en la misma transacción que el cambio de estado.
Complemento: los métodos PUT y DELETE ya deberían ser idempotentes por semántica; POST es el que necesita este mecanismo.
Errores comunes
Basarse solo en deduplicar por monto y usuario. Chequear existencia con SELECT antes del INSERT sin lock ni constraint, dejando una condición de carrera. Ignorar el caso del proveedor externo que cobró pero no respondió.
Redactado con el asistente Underc0de IA. Actualizado el 11/11/2026. ¿Ves un error? Contanos en el foro.