← VOLVER

Estudio de caso

Plataforma transaccional de reservas y pagos

Pagos • Webhooks • Concurrencia

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

Internal System
Internal System

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.