Este proyecto es un ejercicio educativo completo que demuestra, paso a paso, cómo refactorizar una aplicación monolítica hacia una arquitectura de microservicios serverless en la nube de AWS. Está diseñado para estudiantes de programación web y web services hispanoparlantes, y se basa en el sistema FTGO (Food To Go Online) del libro "Microservice Patterns" segunda edición de Chris Richardson.
La premisa es simple pero poderosa: construir primero un monolito funcional, entender sus limitaciones en la práctica, y luego descomponerlo en microservicios independientes usando servicios nativos de la nube. Todo el código, comentarios y documentación están escritos en español para facilitar el aprendizaje.
FTGO es un sistema de pedidos de comida en línea donde:
- Los consumidores hacen pedidos de comida a restaurantes locales a través de una interfaz web.
- Los restaurantes gestionan sus menús y aceptan o preparan los pedidos.
- Los repartidores recogen y entregan los pedidos.
- El sistema procesa los pagos de cada pedido.
Es esencialmente un Uber Eats o Rappi simplificado, con todos los componentes necesarios para ilustrar los problemas reales de una arquitectura monolítica.
El monolito FTGO está implementado como un solo proceso FastAPI en Python 3.13, con una base de datos SQLite3 almacenada en un único archivo llamado ftgo.db. Todos los módulos del sistema — consumidores, restaurantes, menús, pedidos, repartidores y pagos — viven dentro del mismo servidor, comparten la misma base de datos y se despliegan como una sola unidad.
La arquitectura sigue el patrón hexagonal:
- Adaptadores de entrada: API REST (FastAPI Routers) y servidor de archivos estáticos para la UI.
- Lógica de negocio: módulos separados lógicamente (consumidores, restaurantes, menú, pedidos, repartidores, pagos).
- Adaptadores de salida: SQLAlchemy ORM conectado a SQLite3.
- Servicios externos simulados: Stripe para pagos, Twilio para SMS, Amazon SES para email.
| Tecnología | Uso |
|---|---|
| Python 3.13 | Lenguaje de programación |
| FastAPI | Framework web para la API REST |
| SQLite3 | Base de datos relacional (archivo local) |
| SQLAlchemy | ORM para interactuar con la base de datos |
| Pydantic | Validación de datos de entrada/salida |
| uvicorn | Servidor ASGI para ejecutar FastAPI |
| uv | Gestor de paquetes y entornos virtuales |
| HTML/CSS/JS vanilla | Interfaz web sin frameworks |
El esquema de base de datos tiene 7 tablas interrelacionadas:
- Consumidores: clientes que hacen pedidos (id, nombre, email, teléfono, dirección).
- Restaurantes: establecimientos que ofrecen comida (id, nombre, tipo de cocina, horarios).
- Elementos del Menú: platillos de cada restaurante (id, restaurante_id, nombre, precio, disponibilidad).
- Repartidores: couriers que entregan pedidos (id, nombre, vehículo, disponibilidad).
- Pedidos: la entidad central que conecta consumidor, restaurante y repartidor (id, estado, total, dirección de entrega).
- Elementos del Pedido: platillos específicos dentro de un pedido (cantidad, precio unitario, subtotal).
- Pagos: registro de cobros por pedido (monto, método de pago, estado, referencia).
Las relaciones son: un consumidor hace muchos pedidos, un restaurante recibe muchos pedidos y tiene muchos platillos, un repartidor entrega muchos pedidos, un pedido contiene muchos elementos, y cada pedido tiene exactamente un pago.
El pedido es la entidad más compleja del sistema y sigue una máquina de estados:
CREADO → ACEPTADO → PREPARANDO → LISTO → EN_CAMINO → ENTREGADO
↓ ↓ ↓
CANCELADO CANCELADO CANCELADO
El flujo completo es:
- El consumidor selecciona platillos y crea el pedido (estado: CREADO).
- El restaurante acepta el pedido (ACEPTADO) y comienza a prepararlo (PREPARANDO).
- Cuando la comida está lista (LISTO), se asigna un repartidor.
- El repartidor recoge el pedido (EN_CAMINO) y lo entrega (ENTREGADO).
- El consumidor procesa el pago.
El monolito se despliega en una instancia EC2 de Amazon Linux 2023 con un Network Load Balancer (NLB) al frente:
- El NLB recibe tráfico en el puerto 80 y lo reenvía al puerto 8000 de la EC2.
- La aplicación corre como servicio systemd con uvicorn.
- El costo estimado es de ~$25 USD/mes (EC2 t3.micro + NLB).
El libro de Chris Richardson documenta exactamente lo que le pasó a FTGO en la vida real:
-
Complejidad abrumadora: a medida que el código crece, ningún desarrollador puede entender todo el sistema. La clase Order creció a miles de líneas de código.
-
Desarrollo lento: el IDE se vuelve lento, el build tarda mucho, el ciclo edit-build-run-test se alarga.
-
Despliegue doloroso: para corregir un bug en pagos hay que redesplegar toda la aplicación. FTGO solo podía desplegar una vez al mes, mientras Amazon hacía 130,000 deploys por día.
-
Testing difícil: la complejidad del código hace que los tests sean lentos y frágiles. Bugs llegan a producción.
-
Falta de aislamiento de fallos: un memory leak en un módulo tumba toda la aplicación. No hay forma de escalar módulos independientemente.
-
Conflictos de recursos: el módulo de datos de restaurantes necesita mucha memoria, el de procesamiento de imágenes necesita mucho CPU. Con un monolito hay que comprometer.
-
Stack tecnológico fijo: todo está en Python/FastAPI. No se puede usar Go para entregas en tiempo real o Rust para procesamiento intensivo.
-
Espiral descendente: código difícil de entender → cambios incorrectos → código más difícil de entender.
El primer paso de la refactorización es identificar los bounded contexts (contextos delimitados) del sistema. Se identificaron 5 dominios:
| # | Dominio | Responsabilidad | Entidades |
|---|---|---|---|
| 1 | Consumidores | Gestión de clientes | Consumidor |
| 2 | Restaurantes | Gestión de restaurantes y menús | Restaurante, ElementoMenu |
| 3 | Pedidos | Ciclo de vida de pedidos | Pedido, ElementoPedido |
| 4 | Entregas | Gestión de repartidores | Repartidor |
| 5 | Pagos | Procesamiento de pagos | Pago |
Cada dominio se convierte en un microservicio independiente con su propia base de datos, su propio API, su propio pipeline de despliegue y su propio repositorio de código.
La migración a microservicios implica varias decisiones fundamentales:
1. Compute: De EC2 a AWS Lambda En lugar de mantener una instancia EC2 encendida 24/7, cada microservicio se implementa como una función Lambda. Lambda escala automáticamente de 0 a miles de instancias concurrentes, y solo se paga por el tiempo de ejecución real. Para un ejercicio educativo con poco tráfico, el costo es prácticamente $0.
2. Base de datos: De SQLite a DynamoDB (Database per Service) Se aplica el patrón "Database per Service": cada microservicio tiene su propia tabla DynamoDB. Esto elimina el acoplamiento a nivel de datos. Ya no hay foreign keys entre dominios; la integridad referencial se garantiza a nivel de aplicación mediante validaciones HTTP entre servicios.
3. API: De FastAPI Routers a API Gateway Cada dominio expone sus endpoints a través de un API Gateway REST independiente. Los endpoints se mantienen idénticos al monolito (mismas rutas, mismos métodos HTTP, mismos formatos de request/response) para facilitar la migración del frontend.
4. Frontend: De FastAPI Static Files a Lambda + API Gateway El frontend (HTML/CSS/JS vanilla) ya no se sirve desde el monolito. Se implementa como una Lambda propia que sirve el HTML con CSS y JS inline, expuesta a través de API Gateway (mismo patrón serverless que los microservicios). La única diferencia es que ahora el JavaScript invoca múltiples API Gateways en lugar de rutas relativas.
5. Infraestructura como Código: AWS SAM + CloudFormation
Cada microservicio tiene su propio template SAM (template.yaml) que define todos sus recursos: Lambda, API Gateway, DynamoDB, IAM Roles, CloudWatch Logs. Un solo comando sam deploy crea o actualiza toda la infraestructura.
6. CI/CD: GitHub Actions Cada microservicio tiene su propio pipeline de GitHub Actions que se activa solo cuando cambian archivos en su directorio. El pipeline ejecuta tests, empaqueta con SAM y despliega automáticamente.
La migración de SQLite relacional a DynamoDB NoSQL requiere repensar el modelo de datos:
Consumidores y Repartidores: diseño simple con UUID como partition key y un GSI (Global Secondary Index) para búsquedas por email o disponibilidad.
Restaurantes: diseño de tabla única (single-table design) donde el restaurante y sus platillos comparten la misma tabla usando un esquema PK/SK:
PK=REST#<id>,SK=METADATA→ datos del restaurantePK=REST#<id>,SK=MENU#<menu_id>→ cada platillo del menú
Pedidos: mismo patrón de tabla única:
PK=PED#<id>,SK=METADATA→ datos del pedidoPK=PED#<id>,SK=ELEM#<elem_id>→ cada elemento del pedido- GSI por
consumidor_idpara buscar pedidos de un cliente
Pagos: diseño simple con UUID como PK y GSI por pedido_id.
En esta versión se usa comunicación síncrona (HTTP) entre servicios:
- Pedidos → Consumidores: al crear un pedido, el servicio de pedidos llama al API de consumidores para validar que el cliente existe.
- Pedidos → Restaurantes: obtiene el menú y los precios para calcular el total.
- Pedidos → Entregas: al asignar repartidor, marca al repartidor como ocupado.
- Pagos → Pedidos: al procesar un pago, consulta el total del pedido.
El frontend también actúa como orquestador, haciendo llamadas directas a cada microservicio según la operación.
Como evolución futura se podría usar Amazon EventBridge para comunicación asíncrona basada en eventos (patrón Saga).
Al separar las bases de datos, se pierde la transaccionalidad ACID entre dominios. Se acepta consistencia eventual con un patrón Saga simplificado: si falla la validación del consumidor al crear un pedido, se retorna error inmediato sin necesidad de compensación.
Como el frontend y los microservicios se exponen en diferentes API Gateways (diferentes dominios), cada microservicio configura CORS para permitir las llamadas cross-origin. Cada Lambda incluye los headers Access-Control-Allow-Origin: * en todas sus respuestas.
ftgo-microservicios/
├── frontend/ ← Frontend servido por Lambda + API Gateway
│ ├── template.yaml ← IaC: Lambda + API Gateway
│ ├── src/
│ │ ├── handler.py ← Lambda que sirve el HTML
│ │ └── static/
│ │ └── index.html ← HTML con CSS/JS inline
│ └── .github/workflows/deploy.yml
│
├── servicios/
│ ├── consumidores/ ← Microservicio de Consumidores
│ │ ├── template.yaml ← SAM: Lambda + API GW + DynamoDB
│ │ ├── src/handler.py ← Código de la Lambda
│ │ ├── pyproject.toml ← Dependencias (uv)
│ │ └── .github/workflows/deploy.yml
│ ├── restaurantes/ ← Microservicio de Restaurantes + Menú
│ ├── pedidos/ ← Microservicio de Pedidos (el más complejo)
│ ├── entregas/ ← Microservicio de Repartidores
│ └── pagos/ ← Microservicio de Pagos
│
└── scripts/
└── migrar_sqlite_a_dynamodb.py ← Migración de datos
Cada microservicio es autónomo: tiene su propio template de infraestructura, su propio código, sus propias dependencias y su propio pipeline de CI/CD. Esto permite que cada uno pueda vivir en un repositorio separado y eventualmente en una cuenta AWS diferente.
El handler de Lambda es un solo archivo Python que:
- Recibe el evento HTTP del API Gateway.
- Enruta la petición según el método HTTP y la ruta.
- Ejecuta la lógica CRUD contra DynamoDB usando boto3.
- Retorna la respuesta HTTP con headers CORS.
No hay framework web (no FastAPI, no Flask). Lambda + API Gateway reemplazan al framework. El código es Python puro con boto3 como única dependencia externa.
El template SAM define:
- La función Lambda con su runtime (Python 3.13), timeout, memoria y variables de entorno.
- Los eventos del API Gateway (cada ruta/método es un evento).
- La tabla DynamoDB con su esquema de claves y GSIs.
- Los permisos IAM (política DynamoDBCrudPolicy).
- Los outputs (URL del API, ARN de la Lambda, nombre de la tabla).
El servicio de pedidos es el corazón del sistema porque:
- Implementa la máquina de estados completa del pedido.
- Se comunica con otros 3 microservicios (consumidores, restaurantes, entregas).
- Usa el patrón single-table en DynamoDB para almacenar pedidos y sus elementos.
- Calcula totales validando precios contra el menú del restaurante.
La comunicación con otros servicios se hace con urllib.request (librería estándar de Python), sin dependencias externas. Esto mantiene el paquete Lambda pequeño y los cold starts rápidos.
El script migrar_sqlite_a_dynamodb.py es una pieza clave del ejercicio porque demuestra:
- Cómo leer datos de una base relacional (SQLite con sqlite3).
- Cómo transformar registros al formato NoSQL (con PKs/SKs compuestos).
- Cómo manejar el cambio de IDs auto-incrementales (int) a UUIDs (string).
- Cómo mantener un mapeo de IDs viejos a nuevos para preservar las relaciones lógicas.
- Cómo escribir en batch a DynamoDB usando boto3.
El script migra en orden respetando dependencias: primero consumidores y restaurantes (sin dependencias), luego repartidores, después pedidos (que referencian a los anteriores), y finalmente pagos.
Los alumnos trabajan desde una instancia EC2 conectándose por SSH. En esa instancia instalan:
- Python 3.13
- uv (gestor de paquetes)
- AWS CLI v2
- AWS SAM CLI
- Git
Las credenciales AWS se obtienen de IAM Identity Center (antes SSO), que proporciona credenciales temporales (Access Key + Secret Key + Session Token). Esto es más seguro que credenciales permanentes.
Los servicios se despliegan en orden de dependencias:
ftgo-consumidores(sin dependencias)ftgo-restaurantes(sin dependencias)ftgo-repartidores(sin dependencias)ftgo-pedidos(depende de consumidores, restaurantes, repartidores)ftgo-pagos(depende de pedidos)ftgo-frontend(depende de todos — necesita las URLs de los APIs)
Después del primer despliegue, cada servicio se puede actualizar independientemente.
Cada servicio tiene un workflow que:
- Se activa al hacer push a
mainsolo si cambian archivos en su directorio. - Instala dependencias con uv.
- Ejecuta tests con pytest.
- Empaqueta con
sam build. - Despliega con
sam deploy.
El pipeline del frontend empaqueta y despliega la Lambda que sirve el HTML, igual que los demás microservicios.
Después de desplegar todos los microservicios, se actualizan las URLs de cada API Gateway directamente en el JavaScript del HTML. Estas URLs se obtienen de los outputs de cada stack de CloudFormation.
| Aspecto | Monolito | Microservicios |
|---|---|---|
| Compute | EC2 + uvicorn (siempre encendida) | AWS Lambda (pay-per-request) |
| Framework | FastAPI | Lambda handlers nativos (sin framework) |
| Base de datos | SQLite3 (un archivo, todas las tablas) | DynamoDB (una tabla por dominio) |
| API | FastAPI routers (un proceso) | API Gateway REST (uno por dominio) |
| Frontend | Servido por FastAPI | Lambda + API Gateway |
| Despliegue | scp + systemd (manual) | SAM + GitHub Actions (automatizado) |
| Escalamiento | Vertical (instancia más grande) | Automático (Lambda escala a 0 y a miles) |
| Costo en reposo | ~$25/mes (EC2 siempre encendida) | ~$0 (pay-per-request, Free Tier) |
| Aislamiento de fallos | Ninguno (un bug tumba todo) | Total (cada servicio es independiente) |
| Velocidad de despliegue | Todo o nada | Cada servicio por separado |
| IaC | Manual | CloudFormation/SAM (declarativo) |
| CI/CD | No tiene | GitHub Actions (un pipeline por servicio) |
- Despliegue independiente: se puede actualizar el servicio de pagos sin tocar consumidores ni pedidos.
- Escalamiento granular: si pedidos tiene mucho tráfico, solo esa Lambda escala.
- Aislamiento de fallos: si el servicio de pagos falla, los demás siguen funcionando.
- Libertad tecnológica: cada servicio podría usar un lenguaje diferente (aunque aquí todos usan Python).
- Costo optimizado: sin tráfico no se paga nada (vs. $25/mes del EC2).
- Equipos autónomos: cada equipo puede trabajar en su servicio sin coordinarse con los demás.
- Complejidad operacional: ahora hay 5 servicios + frontend que monitorear en lugar de 1.
- Consistencia eventual: ya no hay transacciones ACID entre dominios.
- Latencia de red: las llamadas entre servicios añaden latencia (HTTP entre Lambdas).
- Debugging distribuido: rastrear un error que cruza servicios es más difícil.
- Cold starts: la primera invocación de una Lambda tarda más (inicialización del runtime).
- Duplicación de código: cada servicio tiene su propia lógica de respuesta HTTP y CORS.
- Domain-Driven Design (DDD): identificar bounded contexts para definir los límites de cada microservicio.
- Database per Service: cada servicio es dueño de sus datos. No hay base de datos compartida.
- API-First: los contratos de API se mantienen iguales durante la migración, permitiendo que el frontend funcione sin cambios significativos.
- Infrastructure as Code: toda la infraestructura se define en archivos YAML versionados en Git.
- CI/CD: automatizar el despliegue reduce errores humanos y permite iteraciones rápidas.
- Serverless: eliminar la gestión de servidores permite enfocarse en la lógica de negocio.
Aunque en este ejercicio se hace la migración completa de una vez, en la vida real se usa el patrón Strangler Fig: se migra un módulo a la vez, manteniendo el monolito funcionando para los módulos no migrados, hasta que eventualmente el monolito desaparece.
El diseño está preparado para evolucionar:
- Multi-cuenta AWS: cada dominio puede vivir en su propia cuenta AWS con su propio pipeline.
- Comunicación asíncrona: reemplazar llamadas HTTP síncronas con EventBridge para desacoplar aún más.
- Observabilidad: agregar AWS X-Ray para tracing distribuido, CloudWatch Dashboards para métricas.
- Autenticación: agregar Amazon Cognito para autenticar usuarios.
- Dominio personalizado: usar Route 53 + certificados ACM para URLs amigables.
- Es realista: FTGO es un caso de estudio del libro más referenciado en la industria sobre microservicios.
- Es completo: cubre desde el diseño hasta el despliegue automatizado, pasando por la migración de datos.
- Es accesible: usa Python (lenguaje que los estudiantes ya conocen), sin frameworks complejos.
- Es económico: con el Free Tier de AWS, el costo es prácticamente $0 para uso educativo.
- Es progresivo: primero se entiende el monolito, luego se entiende por qué duele, y finalmente se resuelve con microservicios.
- AWS Lambda: compute serverless (Python 3.13) — tanto para microservicios como para el frontend
- API Gateway: exposición de APIs REST con CORS + hosting del frontend
- DynamoDB: base de datos NoSQL serverless
- CloudFormation/SAM: infraestructura como código
- IAM: gestión de permisos y roles
- CloudWatch: logs y monitoreo
- IAM Identity Center: autenticación federada para los alumnos
- Mostrar el monolito funcionando en EC2 (interfaz web, API, base de datos).
- Explicar los problemas del monolito con ejemplos concretos del código.
- Mostrar la identificación de dominios y el diseño de microservicios.
- Hacer un despliegue en vivo de un microservicio con
sam deploy. - Mostrar la migración de datos ejecutando el script.
- Demostrar el sistema completo funcionando con microservicios.
- Comparar costos, tiempos de despliegue y aislamiento de fallos.
Este proyecto demuestra la transformación completa de un sistema monolítico (Python/FastAPI/SQLite/EC2) a microservicios serverless (Python/Lambda/DynamoDB/API Gateway) en AWS. Es un ejercicio educativo diseñado para estudiantes universitarios que cubre:
- Arquitectura de software: monolito vs. microservicios, patrones de diseño, DDD.
- Cloud computing: servicios serverless de AWS, IaC, CI/CD.
- Ingeniería de datos: migración de SQL relacional a NoSQL, diseño de tablas DynamoDB.
- DevOps: pipelines automatizados, despliegue continuo, infraestructura reproducible.
- Economía cloud: comparación de costos entre modelos de compute.
La narrativa va de lo simple a lo complejo: primero se construye algo que funciona (monolito), se experimenta el dolor de escalarlo, y luego se aplica la solución moderna (microservicios serverless). Este enfoque pedagógico asegura que los estudiantes no solo aprendan el "cómo" sino también el "por qué" de las arquitecturas distribuidas.