Skip to content

🏗️ Arquitectura Multi-Tenant - Solución Web

Esta documentación define la arquitectura de software para una aplicación Software como Servicio (SaaS) multi-tenant, diseñada para ser segura, escalable y observable desde su concepción.


La arquitectura se fundamenta en un stack tecnológico moderno y sigue rigurosamente los principios de la Arquitectura Limpia (Clean Architecture) para garantizar un sistema mantenible y extensible.

🏗️ Clean Architecture

Separación clara de responsabilidades en capas concéntricas con dependencias hacia el interior

🔒 Multi-Tenancy Seguro

Aislamiento de datos mediante Row-Level Security (RLS) en PostgreSQL con Finbuckle.MultiTenant

⚡ Alto Rendimiento

Dapper como micro-ORM para control total sobre SQL y rendimiento predecible

📊 Observabilidad

OpenTelemetry completo con traces, metrics y logs enriquecidos por tenant

ComponenteTecnologíaJustificación
FrameworkASP.NET Core 8Rendimiento, modularidad y ecosistema maduro
Base de DatosPostgreSQL + RLSSeguridad a nivel de motor, escalabilidad
ORMDapperAlto rendimiento, control total sobre SQL
Multi-TenancyFinbuckle.MultiTenantEstrategias flexibles de resolución
ObservabilidadOpenTelemetryVendor-agnostic, instrumentación automática
IdentidadCustom Identity + 2FAControl total, sin dependencias de EF Core

El diseño se basa en capas concéntricas con responsabilidades bien definidas:

  • Domain: Lógica de negocio pura, entidades y reglas empresariales
  • Application: Orquestación de casos de uso y lógica específica
  • Infrastructure: Implementaciones concretas de componentes externos
  • Presentation: Punto de entrada API con controladores y middleware

Principio fundamental: Todas las dependencias apuntan hacia el interior (Domain). Ningún código interno puede referenciar capas externas.

graph TD
    subgraph "Clean Architecture"
        D[Core.Domain
Entidades, Agregados, Interfaces] A[Core.Application
Casos de Uso, DTOs, Interfaces] I[Infrastructure.*
Implementaciones Concretas] P[Presentation.WebAPI
Controladores, Middleware] P --> I P --> A I --> A I --> D A --> D style D fill:#aaffaa,stroke:#3c3 style A fill:#ffffaa,stroke:#cc3 style I fill:#ffaaaa,stroke:#c33 style P fill:#ccccff,stroke:#36c end subgraph "Sistemas Externos" Client[Frontend Vue.js] DB[(PostgreSQL + RLS)] OTEL[OpenTelemetry Collector] P -.->|HTTP/REST| Client I -.->|Dapper/Npgsql| DB I -.->|OTLP| OTEL end
sequenceDiagram
    participant C as Cliente
    participant API as ASP.NET Core API
    participant MT as Tenant Resolver
    participant RLS as RLS Context
    participant DB as PostgreSQL
    participant OT as OpenTelemetry

    C->>API: HTTP Request
    API->>MT: Resolve Tenant (Finbuckle)
    MT->>API: TenantInfo
    API->>RLS: SET app.current_tenant_id
    RLS->>DB: Execute Query with RLS
    DB->>RLS: Filtered Results
    API->>OT: Traces/Metrics/Logs
    API->>C: HTTP Response

La solución se organiza siguiendo Clean Architecture con separación clara de responsabilidades:

📁 Core.Domain/
├── 📄 Entities/ # Entidades de negocio
├── 📄 ValueObjects/ # Objetos de valor
├── 📄 Aggregates/ # Agregados de dominio
├── 📄 Interfaces/ # Contratos de repositorio
└── 📄 IMustHaveTenant.cs # Interface para RLS

Dependencias: Ninguna (cero dependencias externas)

📁 Core.Application/
├── 📄 Services/ # Casos de uso
├── 📄 DTOs/ # Data Transfer Objects
├── 📄 Interfaces/ # Contratos de infraestructura
└── 📄 Mappers/ # Mapeo de entidades

Dependencias: Solo Core.Domain


EstrategiaProsContrasNuestra Elección
DB por TenantAislamiento máximoAlto costo operativo
Schema por TenantBuen aislamientoComplejidad de gestión
RLS CompartidaEficiencia, escalabilidadRequiere configuración cuidadosa

Se implementan múltiples estrategias en orden de prioridad:

  1. Claim Strategy: TenantId desde JWT (máxima seguridad)
  2. Host Strategy: Subdominio (ej. tenant1.app.com)
  3. Path Strategy: Segmento URL (ej. /tenant1/api)
  4. Header Strategy: HTTP Header X-Tenant-ID
// Program.cs - Configuración
builder.Services.AddMultiTenant<AppTenantInfo>()
.WithClaimStrategy("tenant_id")
.WithHostStrategy("__tenant__.__domain__")
.WithBasePathStrategy()
.WithHeaderStrategy("X-Tenant-ID")
.WithStore<CustomDapperTenantStore>();

⚡ Rendimiento

Excepcional: Mapeo con sobrecarga mínima, cercano a ADO.NET nativo

🎯 Control SQL

Total: El desarrollador escribe SQL directamente, optimizaciones finas posibles

📦 Simplicidad

Baja complejidad: Extensión simple sobre IDbConnection

Core.Application
public interface IWriteRepository<in TParameter>
{
Task HandleAsync(TParameter parameter);
}
public interface IReadRepository<in TQuery, TResult>
where TQuery : IQueryParameter<TResult>
{
Task<TResult> HandleAsync(TQuery query);
}
// Unit of Work por contexto de negocio
public interface IInventarioUnitOfWork : IBaseUnitOfWork
{
IProductoRepository Productos { get; }
}
public interface IFinanzasUnitOfWork : IBaseUnitOfWork
{
IFacturaRepository Facturas { get; }
}

  • Recarga en Caliente: Cambios reflejados inmediatamente
  • Jerarquía: Global → Tenant específico
  • Persistencia: PostgreSQL como fuente de verdad
  • Performance: Cache-Aside con Redis
public class PostgresConfigurationProvider : ConfigurationProvider
{
public override void Load()
{
// Cargar desde PostgreSQL
LoadAsync().GetAwaiter().GetResult();
OnReload(); // Notificar cambios
}
private async Task LoadAsync()
{
// 1. Configuraciones globales (tenant_id = NULL)
// 2. Configuraciones por tenant
// 3. Poblar diccionario Data del provider
}
}

Para mantener consistencia con Dapper y evitar dependencias de Entity Framework, se implementa un sistema de identidad completamente personalizado.

// DapperUserStore: Implementa IUserStore<AppUser>
public class DapperUserStore : IUserStore<AppUser>,
IUserPasswordStore<AppUser>,
IUserTwoFactorStore<AppUser>
{
private readonly IDbConnectionFactory _connectionFactory;
public async Task<AppUser> FindByNameAsync(string userName)
{
using var connection = _connectionFactory.CreateConnection();
var sql = "SELECT * FROM users WHERE username = @UserName";
return await connection.QuerySingleOrDefaultAsync<AppUser>(sql,
new { UserName = userName });
}
}

🔍 Traces

Distributed Tracing: Seguimiento de requests a través de servicios

📈 Metrics

Application Metrics: CPU, memoria, request rates, errores

📝 Logs

Structured Logging: Correlación automática con TraceId/SpanId

Program.cs
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddNpgsql()
.AddOtlpExporter())
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddRuntimeInstrumentation()
.AddOtlpExporter())
.WithLogging(logging => logging
.AddOtlpExporter());

ÁreaPráctica RecomendadaJustificación
SecretosAzure Key Vault/AWS Secrets ManagerPreviene exposición en código fuente
Base de DatosUsuario SIN privilegio BYPASSRLSGarantiza que RLS sea inquebrantable
TelemetríaFiltrar PII en OpenTelemetry CollectorCumplimiento GDPR, previene fugas
AutenticaciónRate limiting en login/2FAMitiga ataques de fuerza bruta

🔧 Dapper Optimizations

  • Consultas SQL optimizadas manualmente
  • Parámetros para prevenir SQL injection
  • Connection pooling eficiente

🗄️ Caching Strategy

  • Redis distribuido para datos compartidos
  • Cache local para configuraciones
  • Invalidación por tags tenant-specific

📊 Monitoring

  • APM con métricas de negocio
  • Alertas por tenant/servicio
  • Dashboards operacionales
  1. Horizontal Scaling: Múltiples instancias de API stateless
  2. Database Scaling: Read replicas para consultas pesadas
  3. Caching Layer: Redis Cluster para alta disponibilidad
  4. CDN Integration: Assets estáticos y cacheo geográfico

  • Estructura de proyectos siguiendo Clean Architecture
  • Configuración Finbuckle.MultiTenant con estrategias múltiples
  • Row-Level Security configurado en PostgreSQL
  • Dapper repositories con Unit of Work pattern
  • Custom Identity stores sin EF Core
  • Sistema 2FA TOTP con códigos de recuperación
  • Configuraciones dinámicas con hot reload
  • Cache distribuido con aislamiento por tenant
  • OpenTelemetry configurado con enriquecimiento
  • Gestión de secretos con provider externo
  • Rate limiting en endpoints críticos
  • Monitoring y alertas configurados
  • Documentación de operaciones actualizada