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.
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) |
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
REEMBOLSADOexiste 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 aReservay la confirma cuando el pago se aprueba (RN-03).
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")]
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.
# 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.
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
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 condocker/postgres/01-crear-bd-pagos.sql) y no puede ver las tablas de Django. Por esoreserva_idquedó 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_KEYcon 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_ides único no se puede cobrar dos veces. Si se cobra pero Django no confirma, el pago no se borra: quedaAPROBADOconreserva_confirmada = false, se responde502 PAG-07y queda para conciliar. -
Mismo contrato:
/api/v2/pagos/responde los mismos campos que la v1, así que en el JavaScript solo cambió la URL.
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) |
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.
docker compose up --build
docker compose exec django python manage.py sembrar_demoEntrar 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