Cuando un cliente reintenta una solicitud por timeout, tu API debe responder sin crear efectos secundarios duplicados. Esa es la diferencia entre sistema robusto y caos transaccional.

Patron base

Usa una idempotency-key por operacion y guarda estado del primer procesamiento:

  • request hash,
  • status,
  • respuesta final.

Si llega la misma key, responde lo ya procesado.

Caso practico guiado (TypeScript)

Escenario: endpoint de pagos. Cliente hace retry por timeout y termina cobrando 2 veces.

Implementacion minima:

type IdempotencyRecord = {
  key: string;
  requestHash: string;
  status: "processing" | "done" | "failed";
  response?: unknown;
};

const store = new Map<string, IdempotencyRecord>();

export async function chargePayment(input: { key: string; payload: unknown }) {
  const existing = store.get(input.key);

  if (existing?.status === "done") {
    return existing.response;
  }

  if (existing?.status === "processing") {
    throw new Error("Request still processing, retry later");
  }

  store.set(input.key, {
    key: input.key,
    requestHash: JSON.stringify(input.payload),
    status: "processing",
  });

  try {
    const result = await gatewayCharge(input.payload);
    store.set(input.key, {
      key: input.key,
      requestHash: JSON.stringify(input.payload),
      status: "done",
      response: result,
    });
    return result;
  } catch (error) {
    store.set(input.key, {
      key: input.key,
      requestHash: JSON.stringify(input.payload),
      status: "failed",
    });
    throw error;
  }
}

En produccion ese store debe ir a Redis/DB con TTL.

Errores frecuentes

  • no persistir key por tiempo suficiente,
  • tratar idempotencia solo en frontend,
  • no contemplar fallos parciales.
  • no validar que la misma key venga con el mismo payload.

Checklist

  • idempotency-key obligatoria en endpoints criticos
  • storage de idempotencia con TTL claro
  • retries con backoff + jitter
  • observabilidad por key

Relacionado:

Happy reading! ☕

Comments