Version: 1.0
Fecha: 23 de Mayo, 2026
Estado: NORMATIVO - Detalle tecnico de la migracion multitenant entregada
Arquitecto: Carlos Alberto Torres Camargo
Este documento describe la migracion multitenant fases F1-F5 que se completo en SimappeCommons entre las versiones 3.0.0 y 4.7.4 y que llega a Centrica con stack-2026-05-23. Cubre el alcance, los componentes nuevos, el modelo conceptual y la guia operativa para activarlo de forma controlada.
Audiencia: Arquitecto y desarrolladores backend Centrica que necesiten entender el modelo tenant o planificar la activacion en QA / produccion.
Hasta la version 3.0.0 de Commons, el aislamiento de datos por cliente (tenant) en el ecosistema Simappe estaba resuelto a nivel de base de datos (DB-per-tenant en Admin, esquema-per-tenant en Client). Los servicios accedian a su DB segun la connection que recibian desde SimappeAdmin.DatabaseConfigV2.
El programa multitenant-closure-2026 se concibio para:
database_config_v2 (que conecta a las DBs de los tenants), evitando que un dump de la BD exponga credenciales.Las fases F1 a F5 fueron desarrolladas en SimappeCommons entre los releases 4.0.0 y 4.7.4. stack-2026-05-23 consolida y entrega todas.
Las 5 fases estan entregadas en Commons 4.7.4. En Centrica viajan con flags false por defecto (activacion controlada).
| Componente | Estado |
|---|---|
JpaPasswordEncryptionConverter (@AttributeConverter) |
Listo en Commons 4.7.4. AES-256-GCM. Prefijo enc:v1: para futura rotacion. |
JpaEncryptionAutoConfiguration (ApplicationRunner @Lazy(false)) |
Inicializa la key desde application.security.db-encryption-key al startup. Tolerante a key vacia (loguea WARN, no falla). |
Migration script LegacyPasswordEncryptionMigration en SimappeAdmin |
Reescribe rows existentes (plaintext) a cifrados. Ejecucion opcional. |
| Entidad cubierta | DatabaseConfigV2.password |
| Compatibilidad con plaintext legacy | Si (doble lectura). Rows sin prefijo enc:v1: se leen como plaintext sin tocar la key. |
Cubre los consumidores asincronos que no propagaban el tenant del request original:
@Async y @Scheduled (TaskDecorator + TenantAwareTaskScheduler).Aplicado en SimappeOauth2Server (1.4.8), SimappeGatewayServer (2.1.6) y SimappeAdmin (4.5.4). Centrica recibe esta capacidad y la activa cuando decida.
@ConnectionContext deprecadosSe removio el uso de la anotacion @ConnectionContext en los lightloaders del Admin que ya no era necesario tras F2. Reduce ruido y centraliza la resolucion de tenant en el filter principal.
Filter de Hibernate (@FilterDef + @Filter) que aplica condicion WHERE client_id = :clientId AND company_id = :companyId AND (subsidiary_id = :subsidiaryId OR subsidiary_id IS NULL) automaticamente a todas las consultas sobre entidades que extienden SequenceBusinessBaseEntity o IdentityBusinessBaseEntity.
Property: simappe.multitenant.query-filter.enabled.
| Componente | Detalle |
|---|---|
MongoTenantConfiguration con @ConditionalOnProperty("simappe.multitenant.mongo.enabled", havingValue=true) |
Crea un MongoTemplate distinto por tenant. Cuando esta OFF, se usa el MongoTemplate fallback estandar. |
MongoTenantAutoConfiguration (en SimappeCommons) |
Bean del converter + factories para resolver tenant en runtime. |
MongoTenantFilter |
Procesa requests con contexto INTERNAL ademas de INFRASTRUCTURE. Asegura que rutas internas tambien propaguen tenant. |
| AuditLog tenant fields | AuditLogListener ahora incluye client_id, company_id, subsidiary_id en los eventos Kafka. Bumped en Commons 4.7.2. |
TenantTransactionManager |
@Primary JpaTransactionManager con TransactionTemplate explicito. Soluciona casos de transacciones tenant-aware. |
TenantContext vive en ThreadLocal. Se inicializa en el filter HTTP (segun JWT) y se propaga a:
@Async o @Scheduled (via TaskDecorator).X-Tenant-Context) y consumers (via filter en @KafkaListener).EntityManager.enableFilter("multitenantFilter")).MongoTemplate cuando F5 esta activo).Todas viven en simappeconfig y se sirven via Config Server. Por defecto estan en false en Centrica.
| Propiedad | Default Centrica | Efecto cuando ON |
|---|---|---|
simappe.multitenant.tenant-resolver.enabled |
false | Activa el resolver del filter principal. Sin esto el resto no opera. |
simappe.multitenant.query-filter.enabled |
false | Activa el Hibernate filter para queries automaticas. |
simappe.multitenant.mongo.enabled |
false | Activa Mongo per-tenant. Requiere infra Mongo por cliente (separadas o un cluster con BD por cliente). |
simappe.multitenant.kafka.enabled |
false | Inyecta tenant headers en producers y los consume en listeners. |
simappe.multitenant.audit.enabled |
false | Agrega tenant fields al AuditLog en Kafka. |
Estas propiedades se entregan en el
application-common.ymldelsimappeconfigcon valor explicitofalseendev-config,local-configylocalhost-config.prod-configno se modifica en este upgrade y debe revisarse aparte cuando se decida promover a prod.
Por la magnitud del cambio, se recomienda activar por fases independientes, no todo a la vez:
| Paso | Propiedad a true |
Verificacion |
|---|---|---|
| 1 | simappe.multitenant.tenant-resolver.enabled |
Logs muestran TenantContext resuelto correctamente en cada request. Metrica simappe_tenant_context_missing_total cerca de 0. |
| 2 | simappe.multitenant.audit.enabled |
Eventos en topic user-events incluyen client_id, company_id, subsidiary_id en payload. |
| 3 | simappe.multitenant.query-filter.enabled |
Queries Hibernate generan WHERE con tenant fields. No hay regresion en endpoints que necesiten cross-tenant explicito (revisar uso de @CrossTenant o helpers similares). |
| 4 | simappe.multitenant.kafka.enabled |
Headers tenant viajan en topics. Los consumidores deserializan correctamente. |
| 5 | simappe.multitenant.mongo.enabled |
Solo cuando exista Mongo per-tenant configurado en database_config_v2. Sin esta infra, el servicio falla al arrancar. |
Cada paso requiere reiniciar (o POST /actuator/refresh) los servicios afectados para que tomen la nueva config.
nebula-accounting-core requirio migracion manual al pasar de Commons 3.0.0 a 4.7.4. Categorias de cambios:
| Categoria | Cantidad aproximada | Detalle |
|---|---|---|
wrong number of type arguments; required 2 |
~16 | PageResponseSupport<R, P> ahora tiene 2 type parameters (record + ID). Antes era <R>. |
cannot find symbol en config/ |
~25 | Clases multitenant renombradas o movidas (MultiTenantJpaConfig, AsyncConfig, ClientConfig, ResourceLockConfiguration). |
log has private access in TenantDatabaseManager |
~3 | Campo log renombrado a SERVICE_LOG para evitar colision Lombok @Slf4j en subclases. |
method does not override or implement a method from a supertype |
varios | Firmas de metodos abstract cambiaron en las clases base. |
| Cascade en components/entities | resto | Consecuencia de los anteriores. |
El cliente resolvio los 370 errores y dejo nebula-accounting-core compilando contra Commons 4.7.4. nebula-masters no requirio migracion de codigo, solo bump declarativo.
| Componente | Paquete | Uso |
|---|---|---|
TenantContext |
model.oauth2.dto |
DTO con clientId, companyId, subsidiaryId. Vive en ThreadLocal. |
SequenceBusinessBaseEntity |
commons.api.v1.core.base.entity |
Clase base para entidades con @Id Long + SEQUENCE y soporte multitenant. Incluye setTenantContext(...). |
IdentityBusinessBaseEntity |
idem | Equivalente con @GeneratedValue(IDENTITY). |
MultitenantEntityListener |
commons.api.v1.core.multitenant.listener |
@PrePersist y @PreUpdate para inyectar tenant fields automaticamente. |
TenantDatabaseManager |
commons.api.v1.core.multitenant |
Resolucion de DataSource por tenant. Cache + circuit breaker. |
TenantTransactionManager |
idem | @Primary JpaTransactionManager. |
JpaPasswordEncryptionConverter |
commons.api.v1.core.security.converter |
@AttributeConverter AES-256-GCM. |
PageResponseSupport<R, P> |
commons.api.v1.core.interfaces |
Contrato para components con getPageResponseQuery + getRecord aplanado. |
CrudHtppServletRequestComponent<D, I> |
idem | Contrato CRUD basico. Convive con PageResponseSupport (nombres get vs getRecord intencionalmente distintos). |
| Riesgo | Mitigacion |
|---|---|
| Servicios que NO heredan tenant context en threads asincronos no migrados | Activar primero tenant-resolver + audit y monitorear metrica simappe_tenant_context_missing_total. Si > 0, identificar caller y migrar. |
| Queries que necesitan cross-tenant (jobs administrativos, dashboards globales) | Usar helpers @CrossTenant o EntityManager.disableFilter("multitenantFilter") para esos casos puntuales. |
| Performance de Mongo per-tenant cuando hay muchos tenants | Cache de MongoTemplate por tenant. Configurar pool y eviction adecuados. |
| AuditLog crece desproporcionadamente con tenant fields | Validar retencion del topic Kafka y politica de archivo del Mongo de audit. |
Rotacion accidental de DB_ENCRYPTION_KEY |
La key vive solo en .env del compose. Backupear fuera del repo. Sin la key, los rows con prefijo enc:v1: son ilegibles. |
| Documento | Relacion |
|---|---|
| Estrategias Operativas | Guia de configuracion (.env, profiles common+dev) que habilita la activacion |
| Breaking Changes | Cambios concretos de API que rompen consumidores |
| Incidentes Resueltos | El bug clientProducerFactory se origino en F2; ver detalle |
| Version | Fecha | Autor | Descripcion |
|---|---|---|---|
| 1.0.0 | 2026-05-24 | Carlos Torres | Documento de migracion multitenant F1-F5 para stack-2026-05-23 |