Anatomía de un SaaS serverless en AWS
· 6 min de lectura
Build the Drop es mi SaaS: sincroniza catálogos e inventario entre AliExpress y Shopify. Lo construyo solo. Eso condiciona cada decisión de arquitectura más que cualquier requisito técnico: no hay nadie más para atender una alarma a las tres de la mañana.
Este post no va de servicios concretos, sino de la forma del sistema y del razonamiento que hay detrás. Es el tipo de arquitectura que elegiría hoy para cualquier producto que tenga que mover muchos datos entre APIs ajenas con poco presupuesto y un equipo de una persona.
Diagrama
Shopify ──webhooks──▶ ┌──────────────┐
│ API Gateway │──▶ Función "ingesta"
Panel (usuario) ─────▶│ + auth │ │ valida y encola
└──────────────┘ ▼
┌──────────────┐
Planificador ─── cada N minutos ──────▶│ Cola (SQS) │◀── reintentos / DLQ
└──────────────┘
│ lotes
▼
┌───────────────────────────┐
│ Funciones "sync" │
│ ├─ leen de AliExpress │
│ ├─ calculan el cambio │
│ └─ escriben en Shopify │
└───────────────────────────┘
│
▼
┌───────────────────────────┐
│ Base de datos │
│ estado de cada producto │
│ + último sync aplicado │
└───────────────────────────┘
Cuatro ideas sostienen el diagrama:
| Idea | Qué resuelve |
|---|---|
| Nada se procesa en la petición | La entrada solo valida y encola. Responder rápido a un webhook es lo que evita que Shopify lo reintente o lo desactive. |
| La cola es el centro | Absorbe picos, permite reintentar y separa “qué hay que hacer” de “cuándo puedo hacerlo”. |
| Trabajos idempotentes | Ejecutar dos veces el mismo sync deja el mismo resultado. |
| El estado vive en la base de datos, no en la función | Cualquier función puede morir a mitad de un lote sin perder nada. |
Por qué serverless para un solo-founder
Cuando eres una sola persona, el recurso escaso no es la CPU: es tu atención. Cada servidor es una lista de tareas que no aportan nada al producto: parches de seguridad, discos que se llenan, procesos que se quedan colgados, escalado manual cuando llega un cliente grande.
Con funciones y servicios gestionados, esa lista se reduce casi a cero. A cambio, aceptas tres costes que conviene tener claros desde el principio:
- Límites de ejecución. Una función no puede pasar horas trabajando. Obliga a partir el trabajo en piezas pequeñas, cosa que en este tipo de sistema es justo lo que quieres.
- Arranques en frío. Para procesos en segundo plano dan igual. Para el panel del usuario se notan poco si las funciones son pequeñas.
- Depuración más difícil. No hay un servidor donde entrar a mirar. Sin logs estructurados y trazas por trabajo, estás a ciegas.
El otro argumento es económico: el coste escala con el uso, no con el tiempo. Un SaaS que empieza tiene muchas horas sin tráfico. Pagar por servidores encendidos esas horas es pagar por esperar.
Sincronización masiva: colas, lotes y límites de APIs de terceros
El problema de fondo de Build the Drop no es técnico en sentido clásico: es que dependes de dos APIs que no controlas, cada una con sus límites de peticiones, sus caídas y sus formatos. La arquitectura existe sobre todo para convivir con eso.
Los límites de las APIs mandan
Shopify y AliExpress limitan cuántas peticiones puedes hacer por unidad de tiempo. Si un usuario con miles de productos lanza un sync completo, no puedes hacerlo todo de golpe. Y si tienes muchos usuarios, sus límites se reparten por tienda, pero tu infraestructura es compartida.
Por eso el planificador no ejecuta trabajo: genera mensajes en la cola, y los consumidores los procesan al ritmo que permiten las APIs. Cuando una API responde “demasiadas peticiones”, el mensaje vuelve a la cola con un retraso en lugar de fallar. La cola convierte un límite externo en un simple retraso.
Lotes, no productos sueltos
Procesar producto a producto multiplica las llamadas y los arranques de funciones. Procesar todo el catálogo en una sola ejecución choca con los límites de tiempo. El punto intermedio son lotes de tamaño fijo: lo bastante grandes para amortizar cada ejecución, y lo bastante pequeños para terminar con margen y reintentarse enteros sin drama.
Idempotencia: el sync que se puede repetir
En un sistema con colas y reintentos, todo mensaje se va a procesar más de una vez en algún momento. La pregunta no es si pasará, sino qué ocurre cuando pase.
La regla que sigo es que un trabajo de sync no dice “resta 3 al stock”, sino “el stock debe ser 12”. Se calcula el estado deseado a partir de la fuente, se compara con el último estado aplicado y solo se escribe si hay diferencia. Repetirlo diez veces da el mismo resultado que hacerlo una.
fuente (AliExpress) ──▶ estado deseado ──┐
├─▶ ¿difieren? ──sí──▶ escribir en Shopify
último estado aplicado (BD) ─────────────┘ │ │
no ▼
▼ guardar nuevo estado
nada que hacer
Además, comparar antes de escribir ahorra la mayoría de las llamadas de escritura: en un catálogo grande, en cada pasada solo cambia una parte pequeña de los productos.
Lo que falla, se aparta
Algunos mensajes no van a funcionar nunca: un producto borrado en origen, un token de usuario revocado, un dato con un formato inesperado. Reintentarlos para siempre solo gasta dinero y llena los logs. Tras varios intentos, van a una cola de mensajes fallidos (DLQ), que reviso y desde la que puedo reprocesarlos cuando arreglo la causa.
Costes reales
No voy a dar cifras aquí: dependen del número de tiendas, del tamaño de los catálogos y de la frecuencia de sync, y prefiero publicarlas cuando tenga datos que merezca la pena enseñar.
Lo que sí puedo explicar es de qué depende el coste en una arquitectura así:
| Concepto | De qué depende |
|---|---|
| Ejecuciones de funciones | Número de lotes × duración de cada uno |
| Mensajes en cola | Número de lotes y de reintentos |
| Base de datos | Lecturas y escrituras de estado por producto |
| API Gateway | Webhooks y uso del panel |
| Transferencia de datos | Casi despreciable: son JSON pequeños |
La consecuencia práctica es que el coste crece casi en línea recta con los clientes, y en reposo es prácticamente cero. Para un SaaS que empieza, eso significa que el margen por cliente se conoce desde el primer día. También significa que el mayor riesgo de coste no es el tráfico, sino un bug que reintenta en bucle. Por eso las alarmas de facturación y los límites de reintentos van desde el principio, no “cuando haga falta”.
Lo que no escala (todavía)
Esta arquitectura está pensada para crecer sin tocar nada durante bastante tiempo, pero sé dónde se romperá primero:
- Clientes con catálogos enormes. Un sync completo de un catálogo muy grande sigue siendo lento, porque el cuello de botella es el límite de la API de origen y no mi infraestructura. La solución pasa por sincronizar solo lo que ha cambiado, si la fuente lo permite, en lugar de recorrerlo todo.
- Equidad entre clientes. Con una cola compartida, un cliente grande puede retrasar a los pequeños. El siguiente paso es repartir el trabajo por cliente o por prioridad.
- Observabilidad por cliente. Hoy sé si el sistema funciona; quiero saber, sin consultar logs a mano, cuándo fue el último sync correcto de cada tienda y por qué falló el último que no lo fue.
- Yo. El límite real de un SaaS de una sola persona no es técnico. Cada decisión de arquitectura de este post tiene el mismo objetivo: que el sistema necesite de mí lo menos posible.
La lección que me llevo: en un sistema que depende de APIs ajenas, la arquitectura no es la que hace las cosas rápido, sino la que asume que todo va a fallar y aun así llega al estado correcto. Colas, lotes e idempotencia no son optimizaciones. Son lo que te deja dormir.