Módulo 3 · Diseño de APIs

Lección 14 — Idempotencia y versionado

Compatibilidad hacia atrás y la Idempotency-Key que evita cobrar dos veces.

Publicada
En esta lección
  1. Objetivos
  2. 1. El problema: el reintento que cobra dos veces
  3. 2. Idempotencia por diseño (gratis, sin tabla)
  4. 3. Compatibilidad hacia atrás: qué se puede cambiar sin romper
  5. 4. Idempotencia en el webhook (el gemelo)
  6. Autoevaluación

Stack: DRF · Proyecto: TicketFlow Estado: Publicada Prerrequisito: Lección 13 — REST bien hecho


Objetivos

  1. Implementar Idempotency-Key de verdad: guardar respuesta, reenviar la misma, expirar claves.
  2. Diseñar endpoints idempotentes por naturaleza (PUT, DELETE) y proteger los que no pueden serlo (POST de pago).
  3. Evolucionar la API sin romper clientes (compatibilidad hacia atrás y versionado).

1. El problema: el reintento que cobra dos veces

El cliente envía POST /reservations/{id}/payments/, timeout de red, reintenta — y el servidor ya procesó el primero: dos pagos. Ni tu código ni la cola lo evitan: el cliente reintenta legítimamente (las redes fallan). La solución es el contrato de idempotencia:

POST /api/reservations/{uuid}/payments/
Idempotency-Key: 7f3c9a2e-...     ← genera el CLIENTE, única por intención

Servidor:

  1. La clave no existía → procesa, guarda la respuesta asociada a la clave (con su status code).
  2. La clave existe con el mismo cuerpo del request → devuelve la respuesta guardada (sin reprocesar).
  3. La clave existe con otro cuerpo → 422 Unprocessable (uso incorrecto: la clave es por intención).
  4. Claves viejas se expiran (TTL 24h) — son cache de intenciones, no histórico para siempre.

Almacenamiento: tabla idempotency_key(key PK, request_hash, response_status, response_body, created_at) — transaccional con el proceso (la 10): la clave y el efecto se graban en el mismo commit.

2. Idempotencia por diseño (gratis, sin tabla)

Operación¿Idempotente?Por qué
PUT /events/{uuid} (reemplazo completo)síel mismo PUT dos veces deja el mismo estado
DELETE /reservations/{uuid} (cancelar)sí si lo haces no-op al repetirel cancel() de la 00b ya devolvía False
GETsíno muta
POST /reservations/NO por diseño (crea recurso nuevo)la intent-key lo protege
POST /payments/NO (cobra)la intent-key es OBLIGATORIA

La I3 del proyecto ("solo se paga una vez") se cumple por dos capas: idempotency_key UNIQUE en Payment (BD) + el mecanismo de esta lección (API). Defense in depth de nuevo.

3. Compatibilidad hacia atrás: qué se puede cambiar sin romper

Seguro (clientes viejos siguen vivos): añadir campos a respuestas; añadir endpoints; añadir parámetros opcionales con default; añadir valores nuevos que el cliente viejo ignora.

Rompedor (requiere versión): quitar o renombrar campos; cambiar tipos (string→int); cambiar semántica de un código de error; hacer obligatorio un parámetro; cambiar el formato de paginación.

Regla operativa: los clientes no actualizan cuando tú quieres (apps móviles instaladas, scripts de integradores). Cada cambio rompedor = nueva versión mayor conviviendo:

/api/v1/events/   (congelada, bugfixes de seguridad)
/api/v2/events/   (viva)

Versionado en la ruta (no en header: se comparte y depura mejor). Deprecación anunciada: cabecera Deprecation + fecha de corte documentada, y métrica de uso por versión antes de apagar v1 (Lección 46).

4. Idempotencia en el webhook (el gemelo)

La pasarela reintenta sus webhooks (at-least-once, Lección 30): el mismo evento de pago puede llegar 3 veces. Tu handler: verificar firma → buscar payment.idempotency_key = event_id de la pasarela → si existe, responder 200 y no hacer nada (el reintento exitoso es esperado, no error). Esta simetría cliente/pasarela es la razón por la que I3 existe.


Autoevaluación

  1. ¿Por qué la clave de idempotencia la genera el CLIENTE y no el servidor?
  2. ¿Qué guarda el servidor exactamente para responder idempotente y por qué el request_hash es obligatorio?
  3. ¿Por qué la tabla de claves vive en la misma transacción que el efecto (la 10)? ¿Qué bug produce separarlas?
  4. Enumera 3 cambios "seguros" y 3 "rompedores" sobre tu API actual.
  5. ¿Cómo apagarías v1 con datos en la mano y sin romper a nadie de golpe?

Continúa con los ejercicios. Las solutions.md solo tras intentarlo.