🧪 Problem-Driven Systems Lab
🔁 Caso 16 · Resiliencia

Idempotencia y efectos duplicados

Un reintento por timeout se convierte en un segundo cobro salvo que el servidor pueda distinguir 'es la primera vez que veo esto' de 'ya procese esto'.

OPERATIVO 🐘 PHP🐍 Python🟢 Node.js☕ Java🔵 .NET🐹 Go🦀 Rust

📈 Que cambia para el negocio

Elimina cobros y notificaciones duplicadas por reintentos del cliente, y evita el costo de soporte y devolucion que cada uno genera.

💼 Que demuestra tecnicamente

Muestra que el cliente hace lo correcto al reintentar y que distinguir el reintento es responsabilidad del servidor — y que seis de las siete implementaciones dejan de ser correctas al escalar a dos replicas.

✅ Evidencia que deja

  • Contrasta /charge-unsafe y /charge-idempotent sobre los mismos N reintentos de una misma clave.
  • Hace visible overcharged_cents: la plata que el negocio tendria que devolver, en la unidad en que se discute.
  • Expone /outbox con pendientes y entregados, para ver que el efecto que cruza el boundary no se desincroniza del cargo.

👀 Que mirar al ejecutarlo

  • Si charges_applied es igual a attempts sin clave y exactamente 1 con ella.
  • Si duplicates_prevented da attempts - 1: la prueba de que los reintentos se reconocieron como tales.
  • Si side_effects_emitted baja de N a 1 y el transporte pasa de directo a outbox.
Honestidad: El ledger, la tabla de idempotencia y el outbox son estructuras en memoria (un archivo con flock en PHP), no una base con UNIQUE real. Y hay una asimetria que el caso documenta en vez de esconder: seis de las siete versiones resuelven la carrera dentro de su proceso, asi que con dos replicas dejan de ser correctas. Solo la de PHP, que pone la clave en almacenamiento compartido, sobrevive a eso.

Como esta resuelto en cada stack

Reserva atomica de la Idempotency-Key con la primitiva de cada runtime: INSERT ... ON CONFLICT DO NOTHING modelado con flock en PHP, dict.setdefault bajo Lock en Python, Map con has+set atomico por el modelo de un solo hilo en Node, ConcurrentHashMap.putIfAbsent en Java, ConcurrentDictionary.TryAdd en .NET, sync.Map.LoadOrStore en Go, y HashMap::entry con match exhaustivo en Rust — el unico donde ignorar el resultado no compila. Mas outbox pattern en los siete

StackHealth check localComposeDetalle
🐘 PHP 8.3http://localhost:8100/16/healthcompose.root.ymlREADME del stack
🐍 Python 3.12http://localhost:8200/16/healthcompose.python.ymlREADME del stack
🟢 Node.js 22http://localhost:8300/16/healthcompose.nodejs.ymlREADME del stack
☕ Java 21http://localhost:8400/16/healthcompose.java.ymlREADME del stack
🔵 .NET 8http://localhost:8500/16/healthcompose.dotnet.ymlREADME del stack
🐹 Go 1.23http://localhost:8600/16/healthcompose.go.ymlREADME del stack
🦀 Rust 1.83http://localhost:8700/16/healthcompose.rust.ymlREADME del stack

El expediente completo

El caso no empieza en el codigo: empieza en el sintoma y termina en el postmortem.

← Caso 15Caso 17 →