Taller‐2 - theansw3r/CasaLista-Taller01 GitHub Wiki

El patrón Strangler consiste en no reescribir el monolito de una vez, sino sacar una funcionalidad a un servicio nuevo y poner un proxy adelante que mande esa ruta al servicio nuevo y todo lo demás al viejo. En este taller sacamos el módulo de Pagos de Django a un microservicio en Flask, con Nginx repartiendo el tráfico.

Módulo estrangulado: Pagos

Matriz de decisión

Evaluamos los cinco módulos de CasaLista con los tres criterios del enunciado. A cada uno le pusimos un puntaje de 1 a 3, donde 3 siempre significa "más conviene sacarlo". En acoplamiento el puntaje va al revés: poco acoplamiento da 3.

Módulo Frecuencia de cambio Consumo de recursos Acoplamiento con la BD Total Decisión
Cuentas Baja (1) Bajo (1) Muy alto (1) 3 Mantener en Django
Catálogo Media (2) Medio (2) Alto (1) 5 Mantener en Django
Reservas y agenda Media (2) Medio (2) Muy alto (1) 5 Mantener en Django
Notificaciones Baja (1) Medio (2) Nulo (3) 6 Mantener por ahora
Pagos Alta (3) Alto (3) Bajo (2) 8 Estrangular (Flask)

Justificación

Cuentas. Todo el sistema cuelga de Usuario, Cliente y Profesional: reservas, catálogo y pagos tienen FK hacia ahí, y además es el que firma los JWT. Empezar por ahí sería empezar por lo más difícil.

Catálogo. Es el que más consultas recibe, pero son lecturas simples que se arreglan mejor con caché que con otro servicio. Y Servicio lo usa DetalleReserva con FK.

Reservas. Es el corazón del sistema. El Builder escribe en tres tablas en una sola transacción y bloquea la franja con SELECT ... FOR UPDATE para cumplir RN-01. Partir eso en dos bases de datos es justo lo que no hay que hacer.

Notificaciones. Quedó de segunda: no toca la base de datos, pero no tiene una ruta propia, se dispara adentro de ReservaService. Como Nginx reparte por URL, no había nada que enrutar. Para sacarla tiene más sentido una cola de mensajes que un API REST.

Pagos es el mejor candidato:

  • Cambia mucho: faltan los reembolsos (el estado REEMBOLSADO existe pero no se usa) y la liquidación al profesional, y es probable que toque agregar medios como Nequi o Daviplata. Hoy cada cambio obliga a desplegar todo el monolito.
  • Puede bloquear el monolito: una pasarela real se demora (PSE manda al banco, la tarjeta puede pedir 3-D Secure). Django corre con 3 workers de gunicorn, así que tres pagos lentos al mismo tiempo dejan a los demás usuarios sin poder ni buscar un profesional ni iniciar sesión.
  • Es el menos acoplado: como vimos en la Entrega 1, ningún módulo depende de pagos. Solo tiene una FK a Reserva y la confirma cuando el pago se aprueba (RN-03).

Nueva arquitectura

flowchart LR
    U[Navegador] -->|":8080"| N[Nginx]
    N -->|"/api/v2/pagos/*"| F["Flask<br/>servicio de pagos"]
    N -->|"/api/v1/*, /admin/, /"| D["Django<br/>monolito"]
    N -.->|"/interno/* = 404"| X((bloqueado))
    F -->|"GET /api/v1/reservas/id<br/>JWT del cliente"| D
    F -->|"POST /interno/reservas/id/confirmacion/<br/>token de servicio"| D
    D --> DB1[("Postgres<br/>casalista")]
    F --> DB2[("Postgres<br/>casalista_pagos")]
Loading

Son cuatro contenedores en docker-compose.yml: db, django, pagos y nginx. Solo Nginx publica un puerto. Flask le habla a Django directo por la red interna de Docker (http://django:8000), sin pasar por Nginx.

Solución arquitectónica (Nginx)

# Microservicio nuevo
location /api/v2/pagos/ {
    proxy_pass http://flask_pagos;   # pagos:5000
}

# Solo para llamadas entre servicios, desde afuera no existe
location /interno/ {
    return 404;
}

# Monolito legacy
location / {
    proxy_pass http://django_web;    # django:8000
}

/api/v1/pagos/ no se borró: sigue funcionando en Django mientras dura la migración. El front decide a cuál pagar con la variable URL_PAGOS. En Docker apunta a v2, y si algo sale mal, devolverle el tráfico al monolito es cambiar esa variable, sin tocar código.

Flujo de un pago

sequenceDiagram
    actor C as Cliente
    participant N as Nginx
    participant F as Flask (pagos)
    participant D as Django
    participant P as Pasarela

    C->>N: POST /api/v2/pagos/ + JWT
    N->>F: la ruta es /api/v2/pagos/
    F->>D: GET /api/v1/reservas/{id}/ (mismo JWT)
    D-->>F: la reserva, o 403 / 404
    F->>F: guarda el Pago en PENDIENTE
    F->>P: cobrar(total)
    alt rechazado
        F-->>C: 409 PAG-04
    else aprobado
        F->>D: POST /interno/reservas/{id}/confirmacion/
        D-->>F: reserva CONFIRMADA
        F-->>C: 201 con el pago
    end
Loading

Cómo se logró la separación

En la Entrega 1 dijimos que pagos todavía no estaba listo para separarse por dos cosas: se rompía la transacción entre el pago y la reserva, y revisar que la reserva fuera del cliente pasaba a ser una llamada por red. Así lo resolvimos:

  • Base de datos propia: Flask usa casalista_pagos (se crea con docker/postgres/01-crear-bd-pagos.sql) y no puede ver las tablas de Django. Por eso reserva_id quedó como un número y no como FK.
  • Consultar la reserva: Flask no repite la lógica. Le reenvía a Django el mismo JWT del cliente y Django responde 403 si la reserva es de otro o 404 si no existe, igual que antes. Pensamos validar el JWT en Flask, pero eso era compartir la SECRET_KEY con la que se firman todos los tokens.
  • Confirmar la reserva: Flask llama a POST /interno/reservas/{id}/confirmacion/ con un token de servicio y no con el JWT, para que un cliente no pueda confirmarse la reserva sin pagar. Además, Nginx bloquea /interno/ desde afuera.
  • Sin transacción compartida: el pago se guarda antes de cobrar, y como reserva_id es único no se puede cobrar dos veces. Si se cobra pero Django no confirma, el pago no se borra: queda APROBADO con reserva_confirmada = false, se responde 502 PAG-07 y queda para conciliar.
  • Mismo contrato: /api/v2/pagos/ responde los mismos campos que la v1, así que en el JavaScript solo cambió la URL.

Manejo de errores

Flask responde los errores con el mismo formato que Django:

{"error": {"codigo": "PAG-04", "mensaje": "El monto minimo con tarjeta es $ 1000.00."}}
Código HTTP Cuándo
VAL-000 400 el cuerpo no es JSON o algún campo está mal
HTTP-401 401 no se mandó token
PAG-01 404 el pago no existe
PAG-03 409 la reserva no está pendiente
PAG-04 409 la pasarela rechazó el cobro
PAG-06 503 Django no responde
PAG-07 502 se cobró pero no se pudo confirmar la reserva
HTTP-500 500 error inesperado (el detalle va al log, no al cliente)

Impacto esperado

Lo que se gana:

  • Un pago lento ya no ocupa los workers de Django, así que el resto de la aplicación sigue respondiendo.
  • Si Flask se cae, solo falla pagar. Lo probamos apagando el contenedor: Django siguió respondiendo 200 y Nginx devolvió un 503 en JSON.
  • Pagos se puede desplegar y escalar sin tocar el monolito, y agregar un medio de pago nuevo ya no toca el código de Django.

Lo que cuesta:

  • Cada pago hace dos llamadas por red a Django (leer la reserva y confirmarla).
  • Ya no es todo o nada: puede quedar un pago cobrado sin la reserva confirmada (PAG-07), que hoy toca revisar a mano.
  • Los pagos viejos quedaron en la base de Django y los nuevos en la de Flask.

Cómo probarlo

docker compose up --build
docker compose exec django python manage.py sembrar_demo

Entrar a http://localhost:8080 con [email protected] / casalista2026, hacer una reserva y pagarla. En la pestaña Red del navegador se ve que el pago va a /api/v2/pagos/ (Flask) y el resto a /api/v1/ (Django).

curl http://localhost:8080/api/v1/salud/          # responde Django
curl http://localhost:8080/api/v2/pagos/salud/    # responde Flask
⚠️ **GitHub.com Fallback** ⚠️