Migración a Microservicios (Strangler Pattern) - MiguelAngelMarinMejia1/ArquitecturaDeSoftware-Proyecto GitHub Wiki
El cálculo de lo que cuesta una estadía (noches, tarifa por habitación, impuestos y total) salió
del monolito Django y ahora vive en un microservicio independiente en Flask, expuesto en
/api/v2/cotizacion/. El resto del sistema sigue en Django sin cambios y Nginx reparte el
tráfico entre ambos según la URL.
Se evaluaron los módulos del sistema con los tres criterios del taller. Escala: Baja / Media / Alta. En acoplamiento, "Alto" es malo: significa que el módulo depende mucho de otras tablas.
| Módulo | Frecuencia de cambio | Consumo de recursos | Acoplamiento con la BD | Decisión |
|---|---|---|---|---|
Clientes / identidad (CustomerService) |
Baja | Bajo | Alto (tabla Customer enlazada a User) |
Mantener en Django |
Catálogo y disponibilidad (selectors.py) |
Baja | Medio | Alto (consulta cruzada de reservas y habitaciones) | Mantener en Django |
Crear reserva (ReservationService) |
Media | Medio | Muy alto (transacción sobre 5 tablas) | Mantener en Django |
Pagos y facturación (PaymentService, InvoiceService) |
Media | Medio | Alto (Payment e Invoice ligados a Reservation) |
Mantener en Django (candidato futuro) |
Cotización de precios (PricingPolicy, TaxPolicy) |
Alta | Medio | Nulo (cálculo puro, no lee la BD) | Estrangular (Flask) |
-
Es la regla que más cambia. Tarifas, impuestos y temporadas altas/bajas se ajustan con más
frecuencia que cualquier otra parte del negocio. Hoy
SeasonalPricingPolicycobra la tarifa base todo el año y elTAX_RATEdel 19 % es provisional: son cambios seguros a corto plazo. Aislados en su propio servicio, se despliegan sin reiniciar ni arriesgar el monolito de reservas. - Acoplamiento nulo con la base de datos. El cálculo solo necesita fechas y tarifas por noche. Toda esa información viaja en el JSON de la petición, así que el microservicio no comparte tablas con Django. Es la separación más limpia posible: no hay que dividir la base de datos.
- Se consulta mucho más de lo que se reserva. Un huésped cotiza varias veces antes de comprar. Con el servicio separado, ese tráfico de lectura se puede escalar (más workers o réplicas) sin escalar todo Django.
-
Riesgo bajo. Si el microservicio falla, el flujo de reserva de Django no se cae: sigue
usando su propia
PricingPolicy, que tiene exactamente las mismas reglas.
Los demás módulos se quedan en Django porque dependen de varias tablas relacionadas y de transacciones: separarlos obligaría a partir la base de datos, algo que no se justifica todavía.
flowchart LR
Cliente["Navegador / cliente API"] -->|"http://localhost :80"| Nginx
subgraph Docker["Red interna de Docker Compose"]
Nginx["Nginx<br/>(puerta de entrada)"]
Django["django_web :8000<br/>Monolito Django + DRF"]
Flask["pricing_service :5000<br/>Microservicio Flask + gunicorn"]
DB[("SQLite<br/>volumen sqlite_data")]
Nginx -->|"/api/v2/cotizacion/*"| Flask
Nginx -->|"/api/v1/*"| Django
Nginx -->|"/ (web, admin, static)"| Django
Django --> DB
end
Solo Nginx publica un puerto hacia el exterior. Django y Flask viven en la red interna y no se puede llegar a ellos sin pasar por Nginx. Flask no tiene conexión a la base de datos.
sequenceDiagram
participant C as Cliente
participant N as Nginx
participant F as Flask (pricing_service)
C->>N: POST /api/v2/cotizacion/ (JSON)
N->>F: reenvía la petición (proxy_pass)
F->>F: validation.py valida fechas y habitaciones
alt datos inválidos
F-->>N: 400 {"error": {"code", "detail"}}
else datos válidos
F->>F: pricing.py calcula noches, subtotal, impuestos y total
F-->>N: 200 cotización en JSON
end
N-->>C: respuesta
Cada archivo tiene una sola responsabilidad:
| Archivo | Responsabilidad |
|---|---|
app.py |
Crea la app Flask y define las rutas /api/v2/cotizacion/ y /health/
|
pricing.py |
Reglas de precio portadas de reservation/domain/policies.py (mismas fórmulas, Decimal, redondeo ROUND_HALF_UP) |
validation.py |
Valida y convierte el JSON recibido (fechas ISO, rango válido, precios > 0, ids sin repetir) |
errors.py |
Excepciones del dominio y manejadores que convierten cualquier error en JSON estructurado |
config.py |
Configuración por variables de entorno (TAX_RATE, TIME_ZONE, PORT) |
Contrato de la API
POST /api/v2/cotizacion/
{
"check_in_date": "2026-10-10",
"check_out_date": "2026-10-13",
"rooms": [
{ "room_id": 1, "price_per_night": "180000.00" },
{ "room_id": 5, "price_per_night": "250000.00" }
]
}200 OK
{
"check_in_date": "2026-10-10",
"check_out_date": "2026-10-13",
"nights": 3,
"rooms": [
{ "room_id": 1, "price_per_night": "180000.00", "nights": 3, "line_total": "540000.00" },
{ "room_id": 5, "price_per_night": "250000.00", "nights": 3, "line_total": "750000.00" }
],
"subtotal": "1290000.00",
"tax_rate": "0.19",
"taxes": "245100.00",
"total": "1535100.00"
}Los montos viajan como texto para no perder precisión (igual que la API v1 de Django).
Manejo de errores (resiliencia). Todas las respuestas de error usan el mismo formato que la API v1, así un cliente parsea una sola estructura:
{ "error": { "code": "INVALIDDATERANGE", "detail": "La fecha de salida debe ser posterior a la fecha de entrada." } }| Código HTTP | code |
Cuándo |
|---|---|---|
| 400 | INVALIDPAYLOAD |
El cuerpo no es JSON o falta/está mal un campo |
| 400 | INVALIDDATERANGE |
Salida no posterior a la entrada, o entrada en el pasado |
| 400 | NOROOMSSELECTED |
La lista rooms está vacía |
| 400 | INVALIDPRICE |
Un precio no es numérico o no es mayor que 0 |
| 404 | NOTFOUND |
Ruta inexistente |
| 405 | METHODNOTALLOWED |
Método HTTP incorrecto (ej. GET a /api/v2/cotizacion/) |
| 500 | INTERNALSERVERERROR |
Error inesperado: se registra en el log, el cliente recibe un mensaje genérico y el servicio sigue atendiendo |
-
Dockerfile(raíz): imagen de Django. Al arrancar ejecutamigrate,loaddata seedyrunserver 0.0.0.0:8000. -
pricing_service/Dockerfile: imagen independiente de Flask, con sus propias dependencias, servida congunicorn(2 workers) en el puerto 5000. -
docker-compose.yml: levantadjango_web,pricing_serviceynginx. La base de datos SQLite se guarda en el volumensqlite_data, para que los datos sobrevivan al recrear el contenedor. Para que esto funcione,settings.pyahora leeSQLITE_PATHyALLOWED_HOSTSdel entorno.
# nginx/nginx.conf
upstream django_backend { server django_web:8000; }
upstream pricing_backend { server pricing_service:5000; }
server {
listen 80;
# Microservicio nuevo (funcionalidad estrangulada)
location /api/v2/cotizacion {
proxy_pass http://pricing_backend;
}
# Monolito legacy: API v1
location /api/v1/ {
proxy_pass http://django_backend;
}
# Monolito legacy: sitio web, admin y estáticos
location / {
proxy_pass http://django_backend;
}
}Nginx elige siempre la location con el prefijo más largo que coincida, por eso
/api/v2/cotizacion gana sobre /. Para estrangular otro módulo en el futuro basta con agregar
un servicio en docker-compose.yml, su upstream y una nueva location.
docker compose up --build# Salud del microservicio
curl.exe http://localhost/api/v2/cotizacion/health/
# Cotización (PowerShell)
$body = '{"check_in_date":"2026-12-10","check_out_date":"2026-12-13","rooms":[{"room_id":1,"price_per_night":"180000.00"}]}'
Invoke-RestMethod -Method Post -Uri http://localhost/api/v2/cotizacion/ -ContentType "application/json" -Body $body
# Error estructurado (400)
Invoke-WebRequest -Method Post -Uri http://localhost/api/v2/cotizacion/ -ContentType "application/json" -Body '{}' -SkipHttpErrorCheck
# El monolito sigue respondiendo
curl.exe http://localhost/api/v1/health/
curl.exe http://localhost/api/v1/room-types/
-SkipHttpErrorCheckrequiere PowerShell 7. En Windows PowerShell 5 usacurl.exe -i -X POST http://localhost/api/v2/cotizacion/ -H "Content-Type: application/json" -d "{}".
| Aspecto | Antes (monolito) | Después |
|---|---|---|
| Cambiar una tarifa o impuesto | Editar y redesplegar todo Django | Cambiar TAX_RATE o pricing.py y redesplegar solo pricing_service
|
| Escalar las cotizaciones | Escalar todo el monolito | Subir los workers o réplicas solo de Flask |
| Fallo en el cálculo de precios | Afecta al proceso que atiende todo el sitio | Queda aislado en su contenedor; Django sigue funcionando |
| Punto de entrada | Django directo | Nginx único; los servicios internos no se exponen |
| Base de datos | SQLite compartida | Sin cambios: Flask no la usa, así que no hubo que dividirla |
Costos que asumimos: un salto de red adicional (Nginx → Flask), un servicio más que mantener y monitorear, y durante la transición las reglas de precio existen en dos lugares (Django y Flask) y deben mantenerse iguales.
- Hacer que la página de reserva de Django pida la cotización a
/api/v2/cotizacion/en lugar de usar suPricingPolicylocal, dejando esta última solo como respaldo si Flask no responde. - Cuando todo el tráfico de precios pase por Flask, eliminar
PricingPolicydel monolito. - Agregar autenticación o limitación de peticiones en Nginx: hoy
/api/v2/cotizacion/es público, igual que la API v1. - Evaluar pagos y facturación como el siguiente módulo a estrangular.
| Integrante | Responsabilidad |
|---|---|
| Daniel Giraldo | Microservicio Flask: reglas de precio, validación, errores estructurados y endpoint |
| Miguel Marín | Contenedorización: Dockerfiles, docker-compose.yml y configuración de Django por entorno |
| Johan Peña | Orquestación con Nginx, README y esta página de la Wiki |