Imágenes
Galería no disponible.
Resumen
Backend para Patagonia Dreams — operadora de turismo con +180k pasajeros/año y 7.000+ reseñas cinco estrellas en Google. Construí y lideré la plataforma desde cero: reservas y pagos transaccionales (Mercado Pago, Stripe, Pix), backoffice multi-tenant y sync bidireccional con un panel externo de actividades. El invariante central: una reserva solo está 'pagada' cuando el webhook lo confirma — nunca basado en el estado del cliente. Los webhooks se validan con HMAC y se procesan de forma idempotente por event_id. La disponibilidad se bloquea de forma pesimista (SELECT FOR UPDATE) para serializar reservas concurrentes en el mismo slot. Identidad via AWS Cognito con verificación de token JWKS; toda la config crítica desde AWS Secrets Manager. Stack: Django, DRF, PostgreSQL, AWS (SES, Cognito, Secrets Manager, ECR/K8s).
Arquitectura 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-01Webhooks como única fuente de verdad del estado de pago — el redirect del cliente no puede setear 'pagado'
- ADR-02Bloqueo pesimista (SELECT FOR UPDATE) en el slot de disponibilidad — las reservas concurrentes se serializan, no compiten
- ADR-03Claves de idempotencia en creación de reservas; deduplicación por event_id en todos los webhooks entrantes
- ADR-04Validación HMAC en cada payload de webhook antes de procesarlo
- ADR-05AWS Cognito como único punto de entrada de identidad; ID token verificado con JWKS antes de confiar en datos del usuario
- ADR-06Toda la config crítica via AWS Secrets Manager — sin secrets en código ni repo
Escala y restricciones
- Volumen de requests
- Operadora con +180k pasajeros/año. Reservas en plataforma online + bursts de webhooks hasta ~50/min en pico.
- Concurrencia
- Lock pesimista en la fila de disponibilidad por slot; único escritor para el estado de pago. Sin locking cruzado entre slots.
- Dependencias externas
- Mercado Pago, Stripe, Pix (pagos); Panel externo de actividades (disponibilidad, tarifas y sync bidireccional de reservas); AWS Cognito, SES, Secrets Manager; Google (OAuth, My Business, Merchant Center); Meta. Los webhooks son asíncronos; el estado de pago solo llega por webhook.
- Modos de fallo
- Timeout o demora del proveedor → la reserva queda pendiente hasta el webhook o reconciliación manual. Webhook duplicado → idempotente por event_id. Cognito/Panel caídos → auth degradada o sync de catálogo interrumpida.
- Consistencia de datos
- Una transacción DB para reserva + pago en el webhook. Reserva 'pagada' solo tras webhook; el frontend no puede setear pagado. Sync Cognito ↔ Django via get_or_create y verificación de ID token.
Qué se rechazó explícitamente
- ✕
Frontend o redirect callback como fuente de 'pagado'
Los redirects y el estado del cliente son poco confiables; los reintentos del proveedor y múltiples pestañas permitirían doble aplicación o actualizaciones perdidas.
- ✕
Bloqueo optimista en disponibilidad
La tasa de conflictos en slots muy demandados generaría muchos reintentos y mala UX; el lock pesimista dio comportamiento predecible al load observado.
- ✕
Microservicios por dominio (pagos, reservas, catálogo)
El costo operacional y de consistencia (transacciones distribuidas, consistencia eventual) no se justifica al scale actual; se eligió monolito modular con fronteras claras.
- ✕
Export CSV para operaciones
Riesgo de inyección de fórmulas Excel/CSV; reemplazado por respuesta JSON con datos controlados.
- ✕
Secrets o URLs sensibles en código o repo
Toda la config crítica (FRONTEND_URL, Cognito, Stripe, Panel, etc.) via env desde AWS Secrets Manager.