Estudio de caso
Orquestador de pagos idempotente
Arquitectura backend • Sistemas transaccionales
Imágenes
Galería no disponible.
Resumen
Diseñé e implementé un proceso de pagos seguro para reintentos con estrictas garantías de idempotencia bajo envíos concurrentes. Claves de idempotencia en el request; outbox para llamadas al proveedor; reconciliación por webhook con event_id. Garantía: sin doble cobro ante reintentos del cliente, webhooks duplicados o falla de red entre commit en DB y llamada al proveedor.
Client
↓
API (Django)
↓
PostgreSQL (transactions + idempotency)
↓
Outbox Table
↓
Worker (Celery)
↓
Payment ProviderArquitectura y diseño
Transactional Flow Overview
System Invariants
- ·A payment intent cannot transition from failed to succeeded.
- ·Reservation "paid" is set only after a verified webhook; frontend and redirect cannot set it.
- ·Webhook events are processed idempotently by provider event_id.
- ·Availability for a slot is updated under pessimistic lock (SELECT FOR UPDATE); no optimistic commit.
- ·Idempotency keys are scoped per client and stored; duplicate key returns original response.
- ·Payment and reservation state changes for a webhook occur in a single database transaction.
Architecture Decision Records
- ADR-01Clave de idempotencia requerida para todas las solicitudes de inicio de pago
- ADR-02Outbox para llamadas a proveedores; sin efectos secundarios en el ciclo del request
- ADR-03Procesamiento de webhooks idempotente por event_id del proveedor
- ADR-04Límites de transacción: una transacción DB por transición de estado
- ADR-05Reconciliación y manejo de modos de falla
Escala y restricciones
- Volumen de requests
- Reintentos del cliente y del proveedor; requests y webhooks pueden llegar duplicados o fuera de orden.
- Concurrencia
- Único escritor por clave de idempotencia; outbox para llamadas al proveedor. Sin doble cobro bajo reintentos.
- Dependencias externas
- API del proveedor de pago; webhooks. Posibles fallos de red entre el commit en DB y la llamada al proveedor.
- Modos de fallo
- Timeout o proveedor inalcanzable tras el commit → reintento por outbox. Webhook duplicado → idempotente por event_id. Reintento del cliente → la misma clave devuelve el resultado almacenado.
- Consistencia de datos
- Estado de pago y outbox en la misma DB; commit antes de la llamada al proveedor o al outbox. Sin doble cobro; la clave de idempotencia es la única fuente de resultado para el request.
Qué se rechazó explícitamente
- ✕
Procesamiento simple por request sin claves de idempotencia
Los reintentos y envíos duplicados causarían doble cobro; la clave a nivel de negocio es obligatoria.
- ✕
Manejar reintentos solo en la capa HTTP
El estado de la aplicación puede igualmente doble-aplicarse; la idempotencia debe aplicarse en la capa de orquestación con una clave estable.
- ✕
Depender enteramente de las garantías del proveedor
La semántica de los proveedores varía y puede no garantizar exactly-once; nosotros somos dueños de la garantía de no doble cobro.
- ✕
Procesar efectos secundarios dentro del ciclo del request
Si el proceso muere tras el commit en DB pero antes de la llamada al proveedor, el estado es inconsistente; el outbox desacopla y permite reintento sin re-ejecutar el request.