Imaginá que pagás en línea, la página se traba, hacés clic otra vez… y aparece el miedo de siempre: «¿me habrán cobrado dos veces?». Ese problema —operaciones que se ejecutan dos veces cuando el cliente reintenta— es uno de los más costosos en sistemas de pagos y APIs financieras. La extensión open source quarkus-http-idempotency, publicada en Quarkiverse y disponible en Maven Central, lo resuelve a nivel del servidor sin que tengas que escribir lógica especial: hace que un POST o PATCH se ejecute exactamente una vez, aunque llegue repetido.
El problema: reintentar no es gratis
En la web, los reintentos son inevitables: una conexión que se cae, un timeout, un usuario impaciente que hace doble clic, un cliente HTTP configurado para reintentar automáticamente. Cuando la petición es de solo lectura (GET), reintentar es inofensivo. Pero cuando la petición cambia algo —crear un pedido, cobrar una tarjeta, transferir dinero— ejecutarla dos veces puede significar dos cargos, dos pedidos o dos transferencias.
La dificultad es que el cliente muchas veces no sabe si la primera petición llegó. Si el servidor procesó el cobro pero la respuesta se perdió en el camino, el cliente cree que falló y reintenta. El daño ya está hecho.
La solución: el header Idempotency-Key
La industria resolvió esto con un patrón simple y elegante, popularizado por Stripe y formalizado en un borrador del IETF: el header Idempotency-Key.
La idea: el cliente genera una clave única (por ejemplo, un UUID) y la envía con la petición. El servidor recuerda esa clave. Si llega una segunda petición con la misma clave, el servidor no vuelve a ejecutar la operación: simplemente devuelve la respuesta guardada de la primera vez. El efecto secundario ocurre una sola vez; el cliente recibe siempre la misma respuesta.
Ya cubrimos a fondo el concepto y sus casos límite en artículos anteriores. Este artículo se enfoca en la herramienta que lo implementa en Quarkus de forma lista para producción.
Qué es quarkus-http-idempotency
Es una extensión de Quarkus que implementa el patrón Idempotency-Key de forma transparente. Su mayor virtud: una vez que está en el classpath, no requiere cambios de código. Cualquier POST o PATCH que traiga el header Idempotency-Key se maneja de forma idempotente automáticamente.
Características principales:
- Alineada con la especificación. Sigue el borrador IETF: semántica de errores 409/422/400, huella (fingerprint) del cuerpo de la petición, política de expiración documentada y cuerpos de error en formato RFC 9457 (application/problem+json).
- Segura para APIs multiusuario y multi-tenant. La clave de almacenamiento es un compuesto por llamante: SHA-256(principal ⊕ scope ⊕ clave-cruda). Así, un usuario nunca recibe la respuesta guardada de otro. Incluye aislamiento opcional por tenant mediante un header de scope de confianza.
- Stores intercambiables. En memoria (Caffeine) para un solo nodo, o Redis distribuido para clústeres, detrás de una SPI sencilla.
- Endurecida. Memoria acotada, tamaños máximos para fingerprint y cuerpo guardado, y una lista de exclusión que mantiene los headers con credenciales (Set-Cookie, Authorization, *-token…) fuera de las respuestas almacenadas.
- Lista para reactivo. Funciona con tipos de retorno Uni/asíncronos.
Instalación
Agregá la dependencia a tu pom.xml:
io.quarkiverse.idempotency
quarkus-http-idempotency
${quarkus-http-idempotency.version}
Con la extensión en el classpath, cualquier POST/PATCH que lleve un header Idempotency-Key se maneja idempotentemente. No hace falta tocar tus endpoints.
Guía de uso rápida
Veamos el comportamiento con dos llamadas idénticas:
# Primera llamada — ejecuta la operación
curl -i -H "Idempotency-Key: 8e039f93" -H "Content-Type: application/json" \
-d '{"item":"widget"}' https://api.example.com/orders
# HTTP/1.1 201 Created · Location: /orders/order-1
# Reintento con la MISMA clave — reproduce la respuesta guardada,
# el pedido NO se crea de nuevo
curl -i -H "Idempotency-Key: 8e039f93" -H "Content-Type: application/json" \
-d '{"item":"widget"}' https://api.example.com/orders
# HTTP/1.1 201 Created · Idempotent-Replayed: true
El segundo response llega con el header Idempotent-Replayed: true, indicando que fue una reproducción y no una nueva ejecución.
Cómo se comporta en cada caso
La extensión cubre los casos límite que la lógica trivial suele ignorar:
Situación
Comportamiento
Estado HTTP
Clave nueva
Reserva, ejecuta el handler, guarda y devuelve la respuesta
el del handler
Misma clave, mismo cuerpo, completada
Reproduce el estado, cuerpo y headers guardados
el guardado
Misma clave, mismo cuerpo, aún en curso
Rechaza — hay un reintento concurrente en progreso
409
Misma clave, cuerpo distinto
Rechaza — la clave se reusó para otra petición
422
Clave requerida pero ausente/inválida
Rechaza
400
El caso del 422 es clave: si un cliente reusa una clave de idempotencia para un cuerpo distinto, casi siempre es un bug del cliente. Rechazarlo evita corromper datos silenciosamente.
Seguro para multiusuario y multi-tenant
Un error común al implementar esto a mano es guardar las respuestas en una caché global por clave. El problema: si dos usuarios distintos eligen la misma clave (o un atacante adivina una), uno podría recibir la respuesta del otro —una fuga de datos grave.
quarkus-http-idempotency lo previene haciendo que la clave de almacenamiento sea un compuesto por llamante: SHA-256(principal ⊕ scope ⊕ clave-cruda). El principal es la identidad autenticada; el scope permite aislamiento adicional. Resultado: la clave de un usuario vive en un espacio separado de la de cualquier otro, y nunca se cruzan.
Para escenarios multi-tenant, podés activar aislamiento por tenant mediante un header de scope de confianza, de modo que cada inquilino tenga su propio espacio de idempotencia.
Stores: memoria local o Redis distribuido
La extensión separa la lógica de idempotencia del lugar donde se guardan las respuestas, detrás de una SPI:
- in-memory (por defecto) — caché Caffeine acotada por max-entries. Ideal para un solo nodo o desarrollo.
- redis — para clústeres con varios nodos. Agregá el cliente de Redis y cambiá el store:
# application.properties
quarkus.idempotency.store=redis
io.quarkus
quarkus-redis-client
El store de Redis reserva cada clave con un único round-trip atómico SET NX GET PX (requiere Redis 7.0+). Esto garantiza que, incluso con muchos nodos atendiendo reintentos en paralelo, solo uno gane la reserva y ejecute la operación.
Configuración
Todas las propiedades viven bajo el prefijo quarkus.idempotency.: nombre del header, métodos protegidos, TTLs, *backend del store, scope por tenant y límites de recursos. Un ejemplo típico:
# Activar el store distribuido
quarkus.idempotency.store=redis
# (Ejemplos ilustrativos; consultá la documentación oficial para el
# nombre exacto y los valores por defecto de cada propiedad)
# - nombre del header de idempotencia
# - métodos HTTP protegidos (POST, PATCH)
# - tiempo de vida de las claves guardadas (TTL)
# - límites de tamaño para fingerprint y cuerpo guardado
La referencia completa de propiedades y el modelo de seguridad están en la documentación oficial.
Mejores prácticas
- Generá la clave en el cliente, una por intención de operación. Un UUID v4 nuevo por cada acción del usuario (no por reintento). Todos los reintentos de esa acción comparten la misma clave.
- Protegé solo los métodos que cambian estado. POST y PATCH lo necesitan; GET, HEAD y PUT idempotente por naturaleza, no.
- Definí un TTL acorde a tu ventana de reintentos. Las claves no deben vivir para siempre: lo suficiente para cubrir reintentos realistas (minutos a horas, según el caso).
- Usá Redis en producción con más de un nodo. La caché en memoria no se comparte entre instancias; en un clúster, dos nodos podrían ejecutar la misma operación. Redis con la reserva atómica lo evita.
- Nunca guardes credenciales en la respuesta. La extensión ya excluye headers sensibles por defecto; no lo desactives.
- No reuses una clave para cuerpos distintos. Si cambia la operación, cambiá la clave. El 422 está para atraparte si te equivocás.
Seguridad y robustez
La extensión está endurecida pensando en producción:
- Memoria acotada. El store en memoria tiene un límite de entradas; no crece sin control ante una avalancha de claves.
- Tamaños limitados. El fingerprint del cuerpo y la respuesta guardada tienen topes, evitando que un cuerpo gigante agote la memoria.
- Lista de exclusión incondicional. Headers como Set-Cookie, Authorization y cualquiera que termine en -token nunca se almacenan en la respuesta reproducida, evitando filtrar credenciales entre reintentos.
- Errores estándar. Los rechazos usan RFC 9457 (application/problem+json) con un Link a la documentación, fáciles de consumir por clientes.
Preguntas frecuentes
¿Tengo que cambiar mi código para usarla? No. Con la extensión en el classpath, cualquier POST/PATCH con el header Idempotency-Key se maneja solo.
¿Qué pasa si el cliente no envía el header? Depende de tu configuración: la petición puede procesarse normalmente (sin garantía de idempotencia) o rechazarse con 400 si exigís la clave.
¿Sirve para microservicios? Sí, especialmente. En arquitecturas distribuidas con reintentos automáticos entre servicios, la idempotencia es esencial. Usá el store de Redis para compartir el estado entre instancias.
¿Funciona con código reactivo? Sí, soporta tipos de retorno Uni/asíncronos.



Top comments (0)