Este es el proyecto mas grande que he construido como backend architect freelance.
El cliente necesitaba una plataforma enterprise multi-dominio: pagos, facturacion, certificados, analitica, scheduling, geo, monitoreo, email masivo y WhatsApp — todo integrado, todo trazable, todo en produccion. Mas de 20 microservicios. Mas de 200 endpoints. Entre 50 y 100 mil lineas de codigo.
Este post es el registro tecnico de como lo diseñe, que decisiones tome y por que, y que aprendi operando sistemas a esta escala.
Que necesitaba el cliente
La plataforma tenia que manejar dominios que no tienen nada que ver entre si: pagos con webhooks y auditoria de transacciones, integracion con WhatsApp para notificaciones a usuarios, envio de emails en volumen, generacion de certificados de compliance, analitica y reportes, scheduling con zonas horarias, y un dashboard principal que unificara todo eso.
La escala era enterprise desde el dia uno — no un MVP, no un prototipo. El cliente tenia procesos de negocio reales que iban a depender de este sistema.
Mi rol: backend architect completo. Diseñe la estructura de microservicios, construi los servicios de pagos, email y WhatsApp, implemente el API gateway, y monte la infra Docker. No fue un equipo grande — fui yo liderando las decisiones criticas.
Por que microservicios — y el costo real
La pregunta que siempre me hago antes de proponer microservicios es: ¿los dominios son lo suficientemente dispares para justificar la complejidad operacional?
En este caso la respuesta era si, pero por razones especificas:
Los dominios tienen ciclos de vida distintos. El servicio de pagos tiene requisitos de auditoria estrictos y cambia poco. El servicio de WhatsApp depende de la API de un tercero que cambia con frecuencia. El modulo de analitica tiene cargas pesadas de lectura que no deben afectar los pagos. Mezclarlos en un monolito significa que el deploy de un cambio en reportes puede romper los pagos.
Las cargas son heterogeneas. El escaner de pagos procesa webhooks async con alta frecuencia. El servicio de emails hace bulk sending con rate limiting. El gateway maneja trafico HTTP sincronico. Escalar todo junto no tiene sentido.
El trade-off es real. Mas servicios significa mas complejidad operacional: health checks, distributed logging, circuit breakers, connection pools distribuidos. Si el cliente hubiera tenido dominios similares entre si, un monolito modular hubiera sido la respuesta correcta. No recomiendo microservicios por defecto — los recomiendo cuando los bounded contexts lo justifican.
Arquitectura del sistema
graph TB
subgraph "Clients"
DASH["app-eos (Dashboard)"]
PLUTUS_UI["app-plutus (Payments UI)"]
CERT_WEB["web-certifia (Certificates UI)"]
end
subgraph "API Gateway"
GW["api-gateway\n(Fastify 3.0)\nJWT · Rate limit · Proxy"]
end
subgraph "Core Services (NestJS)"
MEMO["api-memo\n(Core business\nPostgreSQL · Swagger)"]
AEGIS["api-aegis\n(Auth / Infra)"]
ATHENA["api-athena\n(Analytics · Reporting)"]
ATLAS["api-atlas\n(Geo / Location)"]
CERTIFIA["api-certifia\n(Certificates · Compliance)"]
KAYROS["api-kayros\n(Scheduling · Time)"]
ORION["api-orion\n(Monitoring · Observatory)"]
PLUTUS["api-plutus\n(Payments · Billing)"]
end
subgraph "Specialized Workers"
SCANNER_API["payment-scanner-api\n(Fastify · Webhooks)"]
SCANNER_SVC["payment-scanner-service\n(Async worker)"]
EMAILS["service-sender-emails\n(NestJS · Queue)"]
WA["whatsapp-groups-service\n(WA Integration)"]
end
subgraph "Infrastructure"
PROXY["proxy-gateway"]
STORAGE["storage-service"]
HADES["db-hades\n(PostgreSQL shared)"]
HELIOS["cache-helios\n(Redis)"]
end
DASH --> GW
PLUTUS_UI --> GW
CERT_WEB --> GW
GW --> MEMO
GW --> AEGIS
GW --> ATHENA
GW --> ATLAS
GW --> CERTIFIA
GW --> KAYROS
GW --> ORION
GW --> PLUTUS
PLUTUS --> SCANNER_API
SCANNER_API --> SCANNER_SVC
MEMO --> EMAILS
MEMO --> WA
MEMO --> HADES
PLUTUS --> HADES
ATHENA --> HADES
MEMO --> HELIOS
ATHENA --> HELIOS
SCANNER_SVC --> HADES
Veintitres servicios en produccion: nueve microservicios core, cuatro workers especializados, tres frontends, y cuatro componentes de infraestructura.
El API Gateway (Fastify) es el unico punto de entrada para los clientes. Valida JWT, aplica rate limiting, y hace proxy hacia el servicio correspondiente. Simple en concepto, critico en practica — porque cualquier bug ahi afecta todo el sistema.
El dominio mas complejo: pagos con idempotency
api-plutus y el duo payment-scanner-api + payment-scanner-service son el corazon del sistema de pagos.
El flujo es: el gateway de pago externo dispara un webhook hacia payment-scanner-api (Fastify, sin framework overhead, maxima velocidad). El scanner valida la firma del webhook, hace el lookup de idempotency key en base de datos, y si la transaccion no existe aun, la encola para payment-scanner-service. El worker async procesa la transaccion, escribe el registro de auditoria inmutable, y notifica a api-plutus.
Por que idempotency es no-negociable en pagos: los gateways de pago reintentan webhooks cuando no reciben respuesta 200 a tiempo. Si no manejas idempotency, puedes creditar un pago dos veces. La key de idempotency va en la tabla de transacciones con un indice unico — si llega el mismo webhook dos veces, el segundo insert falla con constraint violation y devuelvo 200 sin procesar nada. El gateway externo esta feliz, el sistema no duplica.
Verificacion de firma: cada webhook llega con una firma HMAC. La valido antes de hacer cualquier cosa — si la firma no matchea, rechazo inmediato con 401. Esto previene que alguien envie webhooks falsos para creditar pagos arbitrarios.
Registro de auditoria inmutable: cada transaccion genera un registro append-only. No hay UPDATE en la tabla de auditoria — solo INSERTs. Si una transaccion necesita ser revertida, se crea un registro de reversal, no se modifica el original. Requisito de compliance, no de elegancia tecnica.
WhatsApp y email: servicios que tienen que fallar sin matar al sistema
whatsapp-groups-service y service-sender-emails tienen algo en comun: son side effects. Un usuario hace una accion en la plataforma, y como consecuencia se envia un mensaje de WhatsApp o un email. Si el servicio de WhatsApp esta caido, la accion del usuario no debe fallar.
WhatsApp: el servicio maneja webhooks de mensajes entrantes (usuarios respondiendo), procesamiento async en queue, rate limiting hacia la API del proveedor, y upload/download de media. El rate limiting fue el problema mas complicado — la API de WhatsApp tiene limites por numero de telefono y por tipo de mensaje. Implemente un token bucket en Redis (via cache-helios) para distribuir los mensajes en el tiempo sin superar los limites. Los mensajes que superan el rate se encolan con delay, no se pierden.
Email delivery: service-sender-emails maneja bulk sending sin congestion. El problema clasico del bulk email es que si envias mil correos en paralelo, el servidor de destino te va a bloquear como spam. Implemente un queue con concurrencia limitada, retry logic con backoff exponencial, y Dead Letter Queue para mensajes que fallan consistentemente. Los bounces y unsubscribes los proceso como eventos que actualizo en la tabla de preferencias — no vuelvo a enviarle a alguien que reboto tres veces.
Los dos servicios son fire-and-forget desde el punto de vista del llamador. api-memo publica el evento en queue y sigue. Si el email falla, hay retry. Si el retry falla, hay DLQ. Si el DLQ se llena, hay alerta en api-orion. La plataforma principal sigue funcionando.
NestJS: lo que ganas y lo que te cuesta
Elegir NestJS para los servicios core fue una decision deliberada, no por defecto.
Lo que ganas:
El sistema de modulos con dependency injection es la mayor ganancia a escala. Cada modulo encapsula sus providers, controllers y repositorios. Cuando llegas a veinte servicios y cada uno tiene cinco o diez modulos internos, esa estructura te salva de convertir el codigo en un spaghetti de imports circulares.
Los DTOs con class-validator y class-transformer son otro punto fuerte. En una plataforma con 200+ endpoints, tener validacion de input declarativa en el DTO — y que NestJS la aplique automaticamente via ValidationPipe global — elimina cientos de lineas de validacion manual dispersa por los controllers.
@nestjs/swagger genera la documentacion OpenAPI automaticamente desde los decoradores. En una plataforma de esta escala, mantener documentacion sincronizada a mano es imposible. Que el codigo sea la fuente de verdad de la documentacion es una decision que vale su peso en oro cuando el equipo crece.
Lo que te cuesta:
Bootstrap time. NestJS inicializa su contenedor de DI, resuelve dependencias, conecta providers, y levanta el servidor HTTP. En un servicio simple esto es menos de un segundo. En un servicio con veinte modulos, TypeORM conectando a PostgreSQL, y varios providers anidados, puedes estar en cuatro o cinco segundos de arranque. En produccion con Docker no es un problema. En tests de integracion donde levantas el modulo completo para cada suite, el tiempo se acumula.
Curva de aprendizaje real. NestJS tiene opiniones fuertes sobre como organizar el codigo. Si el equipo viene de Express plano, el salto conceptual de entender modulos, providers, scopes de DI, e interceptors no es trivial. Yo ya conocia el patron bien, pero alguien que llega frio a un proyecto de esta magnitud necesita tiempo.
Para este proyecto, la ganancia en estructura y mantenibilidad justificaba completamente el costo de framework. Con veinte servicios escritos en Express plano, el proyecto se hubiera convertido en veinte formas distintas de resolver el mismo problema.
PostgreSQL compartido: decision pragmatica
Tenia la opcion de darle a cada microservicio su propia base de datos. Eso es la ortodoxia de microservicios — database per service, sin acoplamiento de datos.
Decidi no hacerlo, y lo defiendo.
El cliente no tenia un equipo DevOps que pudiera operar veinte instancias de base de datos independientes. Cada base de datos separada significa backups separados, monitoring separado, SSL separado, y complejidad de migrations multiplicada por veinte. El overhead operacional hubiera sido desproporcionado al beneficio de aislamiento.
db-hades es una instancia PostgreSQL compartida con schemas separados por servicio. api-plutus escribe en el schema plutus. api-memo escribe en memo. Las migraciones con TypeORM son independientes por servicio. El aislamiento logico existe aunque la instancia fisica sea compartida.
El trade-off real es connection pool contention bajo carga alta. Veinte servicios compitiendo por conexiones en una sola instancia puede convertirse en un cuello de botella. Lo mitigue con cache-helios (Redis) para lecturas frecuentes via cache-aside pattern — las queries de datos que cambian poco (catalogos, configuracion, datos de referencia) nunca tocan la base de datos si el cache esta caliente. Bajo carga de produccion normal, la contention nunca fue el cuello de botella.
Si el cliente escala a punto donde necesita aislar las bases de datos, el path de migracion existe — los schemas ya estan separados, es cuestion de mover cada uno a su instancia. Pero ese dia probablemente tambien tendran un equipo DevOps para operarlo.
Lo que aprendi a esta escala
El API gateway es el punto de falla mas costoso. Todo el trafico pasa por ahi. Un memory leak en el gateway, un bug en la validacion de JWT, un rate limit mal configurado — y toda la plataforma se ve afectada. Le dedique mas tiempo a testear el gateway que a cualquier otro servicio individual. Tests de integracion con Supertest, tests de carga con k6, revisiones de codigo mas rigurosas.
Distributed logging no es opcional. Cuando tienes veinte servicios y algo falla, necesitas poder rastrear un request desde el gateway hasta el servicio final en un solo query. Implemente correlation IDs que se propagan en headers entre servicios. Cada log incluye el correlation ID. Cuando llega un bug report, busco el correlation ID y veo exactamente que paso en cada servicio.
Los health checks tienen que ser honestos. Muchos servicios reportan “healthy” aunque no puedan conectarse a la base de datos o a sus dependencias externas. Implemento health checks que verifican de verdad: conectividad a PostgreSQL, conectividad a Redis, y en el caso de WhatsApp, que el token de API sea valido. Si un servicio no puede operar, tiene que decirlo — no reportar healthy y fallar silenciosamente.
La complejidad operacional no se puede ignorar. Veinte servicios en produccion significan veinte puntos donde puede ocurrir un deploy fallido, un crash inesperado, o un agotamiento de memoria. Docker Compose en produccion con restart policies ayuda, pero no reemplaza monitoreo real. api-orion existe precisamente para eso — centralizar alertas y metricas de todos los demas servicios.
La estructura paga dividendos tardios. Las primeras semanas en un proyecto de esta escala se siente lento — configurar el modulo base de cada servicio, definir los DTOs, configurar TypeORM, levantar el Swagger. Pero cuando llegue al mes tres y tuve que agregar un nuevo endpoint al servicio de analitica, el patron estaba claro, los modulos estaban definidos, y la adicion fue rapida. La estructura inicial es una inversion.
Este proyecto me enseño que escalar no es solo agregar mas servicios — es tener claridad sobre que problema resuelve cada uno, como fallan de forma segura, y como el equipo que viene despues puede entender el sistema sin que yo este presente para explicarlo.
Los 20+ microservicios no son un logro en si mismos. Son la consecuencia de haber partido el problema correctamente desde el inicio.