Volver al blog

Arquitectura

Elegir la Arquitectura Correcta para tu SaaS

8 min lecturaEnviaIT Engineering

Cuando un cliente nos pide construir una plataforma SaaS, la primera pregunta nunca es "¿qué framework usamos?" sino "¿cómo debe escalar esto en 18 meses?". La arquitectura que eliges el día uno define el techo de tu producto, la velocidad de tu equipo de desarrollo y, en muchos casos, la viabilidad del negocio.

En los últimos 5 años hemos diseñado e implementado la arquitectura de más de 15 productos SaaS. Algunos arrancaron como MVPs que necesitaban validar una idea en semanas. Otros eran plataformas enterprise con requisitos de compliance y multi-tenancy desde el día uno. La lección más importante que hemos aprendido es que no existe una arquitectura universalmente correcta — existe la arquitectura correcta para tu contexto.

Este artículo no es un tutorial. Es un framework de decisión basado en experiencia real.

Las tres opciones realistas

En la práctica, hay tres caminos probados para un SaaS en 2026. Cada uno tiene trade-offs claros, y entenderlos a fondo es la diferencia entre una plataforma que escala y una que se convierte en deuda técnica.

1. Monolito bien estructurado

El monolito ha vuelto. Después de años de "microservicios para todo", la industria se ha dado cuenta de que un monolito bien organizado resuelve el 80% de los casos. Empresas como Shopify, Basecamp y Hey.com corren sobre monolitos masivos y les va bastante bien.

La clave está en la palabra bien estructurado. Un monolito no es excusa para mezclar lógica de negocio con infraestructura, ni para tener un app.js de 3.000 líneas. Un monolito modular tiene separación de concerns clara, boundaries entre dominios, y la capacidad de extraer módulos a servicios independientes cuando sea necesario.

  • Cuándo elegirlo: MVP, equipos de 1-5 devs, tiempo-to-market agresivo, dominio de negocio que todavía no está completamente definido
  • Stack típico: Next.js full-stack, Rails, Laravel, Django
  • Limitaciones: Escalado vertical tiene techo, deploys acoplados, un bug en billing puede tumbar el módulo de auth
  • Ventajas ocultas: Un solo repo, un solo pipeline de CI/CD, debugging trivial con stack traces completos, onboarding de nuevos devs en días (no semanas)
// Un monolito no significa código desordenado.
// Estructura modular desde el día 1:
src/
  modules/
    auth/        // Login, registro, permisos
      routes.ts
      service.ts
      repository.ts
      auth.test.ts
    billing/     // Suscripciones, pagos
      routes.ts
      service.ts
      stripe.client.ts
      billing.test.ts
    events/      // Core del negocio
      routes.ts
      service.ts
      repository.ts
      events.test.ts
    analytics/   // Métricas y dashboards
      routes.ts
      aggregation.service.ts
      analytics.test.ts
  shared/
    db/          // Modelos y migraciones
    queue/       // Jobs asíncronos
    middleware/  // Auth, rate limiting, logging
    errors/      // Error handling centralizado

El truco está en que cada módulo expone una interfaz pública clara y nunca accede directamente a los internals de otro módulo. Si billing necesita saber quién es el usuario, llama a auth.service.getUserById(), no hace un query directo a la tabla users. Esto parece burocrático al principio, pero te salva la vida cuando necesitas extraer un módulo.

// Cada módulo expone un servicio con interfaz clara
// modules/billing/service.ts
export class BillingService {
  constructor(
    private readonly billingRepo: BillingRepository,
    private readonly authService: AuthService, // Dependencia explícita
    private readonly stripeClient: StripeClient
  ) {}

  async createSubscription(userId: string, planId: string) {
    const user = await this.authService.getUserById(userId);
    if (!user) throw new UserNotFoundError(userId);

    const plan = await this.billingRepo.getPlan(planId);
    const stripeSubscription = await this.stripeClient.subscriptions.create({
      customer: user.stripeCustomerId,
      items: [{ price: plan.stripePriceId }],
    });

    return this.billingRepo.saveSubscription({
      userId,
      planId,
      stripeId: stripeSubscription.id,
      status: 'active',
    });
  }
}

Un consejo práctico: define desde el día uno una regla simple — ningún módulo importa archivos de otro módulo que no sea el index.ts público. Puedes reforzar esto con ESLint rules o con una herramienta como dependency-cruiser. Cuando un dev nuevo hace un import directo a un archivo interno de otro módulo, el CI lo rechaza.

2. Microservicios

Dividir tu sistema en servicios independientes que se comunican entre sí. Cada servicio tiene su base de datos, su deploy y su ciclo de vida. Es la arquitectura que escala equipos grandes y dominios complejos, pero viene con un coste operacional que mucha gente subestima.

  • Cuándo elegirlo: Equipos de 10+ devs, dominios claramente separados, necesidad de escalar partes independientemente, requisitos regulatorios que exigen aislamiento de datos
  • Overhead real: Necesitas observabilidad seria (tracing distribuido con OpenTelemetry, logging centralizado con ELK o Datadog), orquestación de deploys (Kubernetes o ECS), gestión de contratos entre servicios (API schemas, event schemas), y una estrategia clara de testing end-to-end
  • Riesgo: Si divides demasiado pronto, terminas con un "monolito distribuido" — lo peor de ambos mundos. Tienes toda la complejidad de microservicios sin ninguno de los beneficios

Los microservicios son una solución organizacional, no técnica. Si no tienes el problema organizacional, no necesitas la solución.

Para que los microservicios funcionen, necesitas que cada servicio sea realmente independiente. Eso significa:

  • Base de datos propia: cada servicio es dueño de sus datos. Si el servicio de billing necesita datos del servicio de usuarios, se comunica vía API o eventos — nunca accede directamente a la DB del otro servicio.
  • Deploy independiente: puedes deployar el servicio de notificaciones sin tocar el de pagos.
  • Equipo dueño: idealmente, un equipo es responsable de 1-3 servicios end-to-end (dev, deploy, monitoring, on-call).
// Comunicación entre servicios vía eventos (patrón preferido)
// user-service/src/events/publisher.ts
import { EventBridgeClient, PutEventsCommand } from '@aws-sdk/client-eventbridge';

const eventBridge = new EventBridgeClient({ region: 'us-east-1' });

export async function publishUserCreated(user: User) {
  await eventBridge.send(new PutEventsCommand({
    Entries: [{
      Source: 'saas.user-service',
      DetailType: 'UserCreated',
      Detail: JSON.stringify({
        userId: user.id,
        email: user.email,
        plan: user.plan,
        createdAt: new Date().toISOString(),
      }),
    }],
  }));
}
// billing-service/src/events/handler.ts
// El servicio de billing reacciona al evento sin acoplarse al user-service
export async function handleUserCreated(event: UserCreatedEvent) {
  // Crear customer en Stripe
  const stripeCustomer = await stripe.customers.create({
    email: event.detail.email,
    metadata: { userId: event.detail.userId },
  });

  // Guardar referencia local
  await billingRepo.createCustomer({
    userId: event.detail.userId,
    stripeCustomerId: stripeCustomer.id,
  });

  // Si el plan lo requiere, crear trial subscription
  if (event.detail.plan === 'pro-trial') {
    await createTrialSubscription(event.detail.userId, stripeCustomer.id);
  }
}

El coste oculto que nadie menciona: testing. En un monolito, testear un flujo de "usuario se registra y se le crea una suscripción" es un test de integración normal. Con microservicios, necesitas levantar múltiples servicios, gestionar contract tests, y mantener mocks actualizados. Hemos visto equipos que pasan más tiempo manteniendo el entorno de testing que desarrollando features.

3. Serverless / Event-driven

Funciones que se ejecutan bajo demanda, conectadas por eventos. AWS Lambda, API Gateway, EventBridge, DynamoDB. Es el modelo que más ha crecido en los últimos 3 años, especialmente para workloads con patrones de tráfico irregulares.

  • Cuándo elegirlo: Cargas variables (picos de tráfico impredecibles), procesamiento de eventos, integraciones con terceros, equipos que quieren zero ops
  • Beneficio: Escala automáticamente desde 0 a miles de requests concurrentes, pagas solo por uso real, AWS gestiona la infraestructura
  • Limitaciones: Cold starts (mitigables con provisioned concurrency), debugging más complejo que un monolito, vendor lock-in real, límites de ejecución (15 min máximo en Lambda)
// Lambda handler para procesar pagos
// Patrón típico: API Gateway → Lambda → DynamoDB + SQS
import { APIGatewayProxyHandler } from 'aws-lambda';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';

const dynamo = new DynamoDBClient({});
const sqs = new SQSClient({});

export const handler: APIGatewayProxyHandler = async (event) => {
  const body = JSON.parse(event.body || '{}');

  // 1. Validar y persistir
  const order = await saveOrder(dynamo, body);

  // 2. Encolar procesamiento asíncrono
  await sqs.send(new SendMessageCommand({
    QueueUrl: process.env.PAYMENT_QUEUE_URL,
    MessageBody: JSON.stringify({
      orderId: order.id,
      amount: order.total,
      customerId: order.customerId,
    }),
    MessageGroupId: order.customerId, // FIFO ordering por customer
  }));

  // 3. Responder inmediatamente
  return {
    statusCode: 202,
    body: JSON.stringify({ orderId: order.id, status: 'processing' }),
  };
};

El patrón serverless brilla especialmente en procesamiento de background: generar PDFs, procesar imágenes, enviar emails, calcular analytics. Son tareas que no necesitan una respuesta inmediata al usuario y que se benefician enormemente del escalado automático.

Infrastructure as Code es obligatorio. Con serverless, tu infraestructura son decenas de Lambdas, colas SQS, tablas DynamoDB, y reglas de EventBridge. Sin IaC (CDK, Terraform, SST), pierdes el control en semanas.

// Definición con SST (nuestro framework preferido para serverless)
// stacks/ApiStack.ts
import { Api, Queue, Table } from 'sst/constructs';

export function ApiStack({ stack }: StackContext) {
  const ordersTable = new Table(stack, 'Orders', {
    fields: { id: 'string', customerId: 'string' },
    primaryIndex: { partitionKey: 'customerId', sortKey: 'id' },
  });

  const paymentQueue = new Queue(stack, 'PaymentQueue', {
    consumer: 'packages/functions/src/processPayment.handler',
  });

  const api = new Api(stack, 'Api', {
    routes: {
      'POST /orders': {
        function: {
          handler: 'packages/functions/src/createOrder.handler',
          bind: [ordersTable, paymentQueue],
        },
      },
    },
  });
}

Tabla comparativa

Para facilitar la decisión, esta tabla resume los trade-offs principales:

| Criterio | Monolito modular | Microservicios | Serverless | |----------|-----------------|----------------|------------| | Time to market | Muy rápido | Lento (setup inicial alto) | Rápido (si dominas el stack) | | Coste inicial | Bajo | Alto | Medio | | Coste a escala | Medio-Alto | Medio | Bajo (pay-per-use) | | Complejidad operacional | Baja | Muy Alta | Media | | Escalado | Vertical (con techo) | Horizontal por servicio | Automático | | Debugging | Trivial | Complejo (distributed tracing) | Medio (CloudWatch + X-Ray) | | Onboarding devs | 1-2 días | 1-2 semanas | 3-5 días | | Tamaño de equipo ideal | 1-8 devs | 10+ devs | 2-6 devs | | Vendor lock-in | Bajo | Bajo-Medio | Alto | | Testing E2E | Simple | Complejo | Medio |

Framework de decisión

Cuando un cliente nuevo llega con una idea de SaaS, seguimos este proceso de decisión:

Paso 1: Entender la carga esperada. Si el SaaS va a tener menos de 10.000 usuarios activos en los primeros 12 meses, un monolito modular es casi siempre la respuesta correcta. No hay debate.

Paso 2: Evaluar el equipo. Si el equipo es de 1-5 devs, microservicios están descartados. No importa cuánto quieran hacerlo "bien desde el principio". El overhead operacional consume más tiempo del que ganas en independencia de deploy.

Paso 3: Analizar los patrones de tráfico. Si el tráfico es predecible (app B2B donde la gente trabaja de 9 a 6), un monolito con auto-scaling básico es suficiente. Si el tráfico es impredecible (marketplace, plataforma de eventos), serverless empieza a tener sentido.

Paso 4: Considerar el dominio. Si el negocio tiene dominios claramente separados (un marketplace con vendedores, compradores, y logística como mundos independientes), la separación en servicios tiene sentido — pero no necesariamente desde el día uno.

Paso 5: Planificar la evolución. La mejor arquitectura es la que permite evolucionar. Un monolito modular se puede dividir en servicios. Un serverless bien diseñado se puede mover a containers. Pero un monolito desordenado o unos microservicios mal particionados son deuda técnica que cuesta meses arreglar.

Caso real: Plataforma de gestión de suscripciones

Un cliente llegó a EnviaIT con la idea de construir una plataforma SaaS de gestión de suscripciones para negocios físicos (gimnasios, academias, coworkings). Necesitaban: registro de miembros, control de acceso con QR, pagos recurrentes con Stripe, dashboard para el dueño del negocio, y una app móvil para los miembros.

Decisión arquitectónica: monolito modular con Next.js para el dashboard web, una API compartida, y Flutter para la app móvil. Todo corriendo en AWS con ECS Fargate.

El argumento para microservicios era tentador: "el módulo de pagos debería ser independiente, el de control de acceso tiene requisitos de latencia diferentes, y el dashboard tiene patrones de tráfico distintos". Todo cierto. Pero el equipo era de 3 personas y necesitábamos el MVP en 8 semanas.

Arrancamos con un monolito. En el mes 6, cuando ya teníamos 200 negocios usando la plataforma y el equipo creció a 7 personas, extrajimos el módulo de notificaciones a un servicio serverless (Lambda + SQS) porque era el único módulo que tenía patrones de tráfico radicalmente diferentes — picos de miles de notificaciones push a las 7am cuando los gimnasios abren.

En el mes 12, extrajimos el módulo de pagos a su propio servicio porque necesitábamos cumplir con PCI DSS y era más fácil auditar un servicio aislado.

El monolito core sigue siendo un monolito, y funciona perfectamente. No hay planes de dividirlo más.

Resultado: MVP en 7 semanas. Primer cliente pagando en la semana 9. Cero downtime en 12 meses. Coste de infraestructura mensual: $180 para los primeros 200 clientes.

Patrones que siempre implementamos

Independientemente de la arquitectura elegida, hay patrones que implementamos en cada SaaS:

  1. Feature flags desde el día uno. Usamos LaunchDarkly o una solución custom con DynamoDB. Esto permite lanzar features a un porcentaje de usuarios, hacer rollback instantáneo sin deploy, y separar el deploy del release.

  2. Multi-tenancy a nivel de datos. Cada tenant tiene su data aislada. En monolitos usamos un tenantId en cada tabla con row-level security en PostgreSQL. En microservicios, cada request lleva el tenantId en el contexto.

  3. Rate limiting y circuit breakers. Especialmente para integraciones con terceros (Stripe, SendGrid, Twilio). Un tercero que se cae no debe tumbar tu plataforma.

  4. Health checks y graceful shutdown. Parece básico, pero la cantidad de SaaS que hemos auditado donde un deploy tumba conexiones activas es sorprendente.

// Health check pattern que usamos en todos los servicios
app.get('/health', async (req, res) => {
  const checks = {
    database: await checkDatabase(),
    redis: await checkRedis(),
    stripe: await checkStripeApi(),
  };

  const healthy = Object.values(checks).every(c => c.status === 'ok');

  res.status(healthy ? 200 : 503).json({
    status: healthy ? 'healthy' : 'degraded',
    checks,
    version: process.env.APP_VERSION,
    uptime: process.uptime(),
  });
});

Nuestra recomendación

Para el 90% de los SaaS que construimos: empieza con un monolito modular, extrae servicios cuando los datos lo justifiquen. Hemos visto más startups morir por over-engineering que por un monolito que escala "poco".

Los datos que justifican extraer un servicio son:

  • Performance: un módulo necesita escalar independientemente porque tiene patrones de carga diferentes
  • Organizacional: un equipo de 4+ personas trabaja exclusivamente en ese dominio
  • Compliance: un módulo necesita aislamiento por requisitos regulatorios (pagos, datos médicos)
  • Tecnología: un módulo se beneficia de un stack diferente (ML con Python, procesamiento de video con Go)

Si no tienes al menos uno de estos factores, el módulo se queda en el monolito. Así de simple.

La clave no es la arquitectura perfecta — es la arquitectura que te permite iterar rápido, medir resultados, y evolucionar sin reescribir todo.


¿Estás planificando tu plataforma SaaS? Hablemos sobre qué arquitectura tiene sentido para tu caso específico.