Patrones de arquitectura limpia para aplicaciones SaaS modernas

Descubre cómo aplicar Clean Architecture en SaaS para lograr mantenibilidad, escalabilidad y desacoplamiento. Incluye ejemplos prácticos en Node.js y TypeScript.

Cloud & Edge
ScalabilityEdgePerformance

Patrones de arquitectura limpia para aplicaciones SaaS modernas

¿Tu empresa está lista para IA? Descargá nuestro checklist gratuito →

Descargar checklist

Introducción

En el mundo del desarrollo de software como servicio (SaaS), la arquitectura limpia se ha convertido en un pilar fundamental para construir aplicaciones que sean fáciles de mantener, escalar y evolucionar. En Tanok Tech, hemos adoptado estos principios para asegurar que nuestras soluciones SaaS no solo cumplan con los requisitos actuales, sino que también se adapten rápidamente a los cambios del mercado.

La arquitectura limpia, popularizada por Robert C. Martin (Uncle Bob), propone una separación clara de responsabilidades en capas, donde las reglas de negocio son independientes de los frameworks, bases de datos y otras preocupaciones externas. En este artículo, exploraremos patrones concretos aplicados a SaaS modernos, con ejemplos en Node.js y TypeScript.

¿Por qué Clean Architecture en SaaS?

Las aplicaciones SaaS suelen crecer rápidamente en funcionalidades y usuarios. Sin una arquitectura sólida, el código se vuelve difícil de modificar y probar. La arquitectura limpia ofrece:

  • Independencia de frameworks: Puedes cambiar de framework sin afectar las reglas de negocio.
  • Testabilidad: Las capas internas pueden probarse sin infraestructura externa.
  • Independencia de la UI: La interfaz de usuario puede variar sin cambios en la lógica central.
  • Independencia de base de datos: Puedes migrar de MongoDB a PostgreSQL sin reescribir la lógica.

Patrón de Capas

La arquitectura limpia se organiza en capas concéntricas, donde las capas externas dependen de las internas, nunca al revés. En un SaaS típico, definimos:

  1. Entidades (Entities): Objetos de negocio centrales, como Usuario, Suscripción, Factura.
  2. Casos de uso (Use Cases): Lógica de aplicación específica, como CrearUsuario, ProcesarPago.
  3. Adaptadores (Adapters): Traducen datos entre casos de uso y el mundo exterior (controladores, repositorios, gateways).
  4. Frameworks y drivers: Express, MongoDB, Stripe SDK, etc.

Ejemplo de Entidad

// entities/Usuario.ts
export class Usuario {
  constructor(
    public readonly id: string,
    public readonly email: string,
    public readonly nombre: string,
    private _plan: Plan,
  ) {}

  get plan(): Plan {
    return this._plan;
  }

  cambiarPlan(nuevoPlan: Plan): void {
    // Reglas de negocio: validar cambios de plan
    if (nuevoPlan === Plan.Gratuito && this._plan === Plan.Premium) {
      throw new Error('No se puede degradar de Premium a Gratuito sin cancelar');
    }
    this._plan = nuevoPlan;
  }
}

export enum Plan {
  Gratuito = 'Gratuito',
  Basico = 'Básico',
  Premium = 'Premium',
}

Esta entidad encapsula reglas de negocio. No depende de nada externo.

Caso de Uso

// use-cases/CrearUsuario.ts
import { UsuarioRepository } from '../adapters/repositories/UsuarioRepository';
import { Usuario, Plan } from '../entities/Usuario';

export class CrearUsuario {
  constructor(private usuarioRepository: UsuarioRepository) {}

  async execute(email: string, nombre: string): Promise<Usuario> {
    // Regla de negocio: email único
    const existe = await this.usuarioRepository.buscarPorEmail(email);
    if (existe) {
      throw new Error('El email ya está registrado');
    }
    const usuario = new Usuario(crypto.randomUUID(), email, nombre, Plan.Gratuito);
    await this.usuarioRepository.guardar(usuario);
    return usuario;
  }
}

El caso de uso depende de una abstracción (UsuarioRepository), no de una implementación concreta de base de datos.

¿Querés un diagnóstico personalizado? Completá el checklist gratuito →

Descargar checklist

Adaptadores: Repositorio

// adapters/repositories/MongoUsuarioRepository.ts
import { UsuarioRepository } from './UsuarioRepository';
import { Usuario } from '../../entities/Usuario';
import { MongoClient } from 'mongodb';

export class MongoUsuarioRepository implements UsuarioRepository {
  constructor(private db: MongoClient) {}

  async buscarPorEmail(email: string): Promise<Usuario | null> {
    const doc = await this.db.db().collection('usuarios').findOne({ email });
    if (!doc) return null;
    return new Usuario(doc.id, doc.email, doc.nombre, doc.plan);
  }

  async guardar(usuario: Usuario): Promise<void> {
    await this.db.db().collection('usuarios').insertOne({
      id: usuario.id,
      email: usuario.email,
      nombre: usuario.nombre,
      plan: usuario.plan,
    });
  }
}

Inversión de Dependencias con IoC

Para lograr la independencia, usamos inversión de control (IoC) con un contenedor, por ejemplo awilix o tsyringe:

// di/container.ts
import { container } from 'tsyringe';
import { UsuarioRepository } from '../adapters/repositories/UsuarioRepository';
import { MongoUsuarioRepository } from '../adapters/repositories/MongoUsuarioRepository';
import { CrearUsuario } from '../use-cases/CrearUsuario';

container.registerSingleton<UsuarioRepository>('UsuarioRepository', MongoUsuarioRepository);
container.registerSingleton(CrearUsuario);

Manejo de Eventos en SaaS

En SaaS, las operaciones suelen desencadenar eventos (email de bienvenida, facturación, etc.). La arquitectura limpia facilita un patrón de eventos:

// domain/events/UsuarioCreadoEvent.ts
export class UsuarioCreadoEvent {
  constructor(public readonly usuarioId: string) {}
}

// use-cases/CrearUsuario.ts (extendido)
import { EventBus } from '../adapters/events/EventBus';

export class CrearUsuario {
  constructor(
    private usuarioRepository: UsuarioRepository,
    private eventBus: EventBus
  ) {}

  async execute(email: string, nombre: string): Promise<Usuario> {
    // ... validación y creación
    await this.usuarioRepository.guardar(usuario);
    await this.eventBus.publish(new UsuarioCreadoEvent(usuario.id));
    return usuario;
  }
}

Recomendaciones para Equipos

  1. No sobreingeniería: No todas las partes del sistema requieren capas completas. Para CRUD simples, puedes simplificar.
  2. Pruebas unitarias: Prueba entidades y casos de uso sin infraestructura. Usa mocks para repositorios.
  3. Documentación de contratos: Define interfaces claras en las fronteras de capa.
  4. Monorepo: Si usas monorepo, separa las capas en módulos internos (packages) para forzar dependencias.

Ejemplo Completo en GitHub

Puedes ver un ejemplo completo en nuestro repositorio SaaS Clean Architecture Example (enlace ficticio).

Conclusión

La arquitectura limpia no es una bala de plata, pero para aplicaciones SaaS complejas ofrece una base sólida para el crecimiento. Al desacoplar la lógica de negocio de los detalles técnicos, tu equipo podrá iterar más rápido y con menos miedo a romper funcionalidades existentes. En Tanok Tech, hemos visto cómo este enfoque reduce la deuda técnica y facilita la incorporación de nuevos desarrolladores.

¿Interesado en implementar estos patrones en tu proyecto? Contáctanos o revisa la documentación oficial de Clean Architecture de Uncle Bob.

Referencias

¿Listo para dar el próximo paso? Evaluá tu empresa con nuestro checklist gratuito →

Descargar checklist

Publicaciones relacionadas