Estado: NORMATIVO. Documento 11 del patrón canónico de feature backend.
Audiencia: equipo de fábrica (desarrolladores backend).
Propósito: catálogo de antipatrones reales observados en el ecosistema, con su corrección canónica. No es un registro de culpas: es la evidencia de por qué cada regla existe. La mayoría apareció porque faltaba un estándar único; documentarlos es lo que impide que se repitan.
Cada antipatrón se describe en tres líneas: Síntoma (cómo se manifiesta), Por qué es un defecto (el riesgo real) y Patrón correcto (la regla que lo previene, con el documento donde está desarrollada). Si al construir una feature reconoces el síntoma, ya sabes que estás fuera del patrón.
@Filter de Hibernate para multitenantfindById, findByCodigo, findByEstado...) sin filtrar tenant, asumiendo que un filtro global de Hibernate aísla las filas.@Filter multitenant no se aplica en este stack. La consulta devuelve filas de otros clientes → fuga de datos entre tenants, el defecto más grave posible.@Query(nativeQuery=true) filtrando client_id + company_id + subsidiary_id extraídos del token (SimappeJwt). Ver 09 §2 y 04.SequenceBaseEntity (sin columnas de tenant) tratándola como catálogo compartido entre clientes.SequenceBusinessBaseEntity (hereda client_id/company_id/subsidiary_id) + DDL con UK por tenant. Ver 09 §1.loadForId) usa el findById heredado de commons; un componente de exportación hace findAll() para armar el CSV/Excel/PDF.*AndTenant nativas, tomando el tenant del token vía RequestContextHolder + SimappeJwt, sin alterar las firmas @Override. Ver 09 §2.TenantContext la conexión cae al datasource por defecto (no el del cliente) → resultados incorrectos o errores de esquema.TenantIterationSupport.forEachActiveTenant, enrutando explícitamente al datasource de cada tenant por iteración. Ver 04.getPageResponseQuery/getPageDtoQuery delegan en el default de PageResponseSupport (this.getPageResponseQuery(query, viewRepository, mapper::toViewResponse)), que paginan por findAll(Specification, Pageable).findAll no filtra tenant. La vista/entidad extiende SequenceBusinessBaseEntity, que no es TenantAware, así que la paginación devuelve filas de todos los clientes → fuga cross-tenant en el endpoint más usado por el front (listados). Es el mismo defecto de fondo que AP-1.1, pero camuflado dentro de un helper de commons que parece "seguro por defecto".NativeTenantPaginator (getPageResponseNative / getPageDtoNative), que inyecta SIEMPRE client_id + company_id + NVL(subsidiary_id,-1) del token en SQL nativo, con búsqueda 1..N y orden 1..N; aplicar un orden estable por defecto cuando el cliente no envía ordersBy. Las firmas REST no cambian. Ver 09 §2 y 03 §2.10.status vs estado de negociostatus del DTO o exponerlo como estado de negociomapDtoToEntity hace entity.setStatus(dto.getStatus()); un Response/record expone status; una query filtra "activos" por status.status (de SequenceBusinessBaseEntity) es ciclo de vida técnico interno, no negocio. Mapearlo desde un payload que no lo trae provoca ORA-01407 (NOT NULL) al actualizar; exponerlo acopla el front a un detalle interno.estado_embargo, estado_consecutivo, estado_tercero...), en español y con enum dedicada. status nunca se mapea desde el DTO, nunca aparece en el contrato, nunca es filtro de negocio. En create toma su valor por defecto; en update se preserva. Ver 09 §3.MultiTenantJpaConfig con DEFAULT_SCHEMA fijo, generateDdl=true, dialecto autodetectado y scan de paquetes amplio.nebula-accounting-core: dialecto Oracle explícito, sin DEFAULT_SCHEMA, hbm2ddl=none, scan acotado a v1.*.entity. Ver 03.mapDtoToEntity asigna el id (p. ej. System.currentTimeMillis()) cuando el DTO no lo trae.create con uninitialized version value antes de tocar la secuencia.@SequenceGenerator + Hibernate; el mapper no toca el id. Ver 03.Authorization crudo a SimappeJwtSimappeJwt.getUserSession(request.getHeader(AUTHORIZATION)) con el valor "Bearer <jwt>" completo.MalformedJwtException ("may not contain whitespace"). Se detecta solo al ejecutar la API real, no en pruebas con mock.SimappeJwt.extractToken(...) antes de resolver la sesión, en todos los puntos. Ver 04.@Immutable sin GRANT al usuario de aplicaciónGRANT SELECT al usuario runtime.@Immutable mapeada a esa vista hace fallar el arranque del servicio.GRANT SELECT al usuario de aplicación es parte del DDL de toda vista. Ver 06.ResponseEntity<byte[]> (csv/xlsx/pdf) responde 500.SuccessResponse, que no es casteable a byte[].@IgnoreResponseBinding para excluirlo del binding y devolver el binario crudo. Ver 09.null al frontnombreCompleto (u otro campo derivado de un módulo externo) en null.get-record / page-response leyendo de la entidad transaccional@Immutable + PageResponseSupport, mapeando vía toViewResponse. Ver 03 y 06.ORA-00001); o falta el índice que garantiza un invariante (un número no debe emitirse dos veces).confirmar() acepta una reserva porque su enum sigue en RESERVADA, sin comprobar la fecha de expiración.expiraEn) además del estado, rechazando con mensaje i18n. Ver 04.estado = "Activo", principal = "Sí") en vez de exponer el código + un *Nombre aparte; o el campo de estado se llama genérico (estado) y no como la columna (estadoTipoPlan). El front, que solo ve ese campo, filtra por el texto ({"key":"principal","value":"Sí"}).WHERE contra la columna real con el valor descriptivo. Si la columna es numérica/booleana (CASE … THEN 1 ELSE 0), Oracle intenta convertir "Sí" → ORA-01722: invalid number (500); si es texto, no machea → 0 filas silenciosas. El mismo síntoma aparece en escritura cuando el front no encuentra el campo de estado y manda status (técnico, ignorado) → la operación no surte efecto.estadoTipoPlan enum, principal boolean), y se agrega <campo>Nombre solo para mostrar. El front filtra por el crudo, pinta el *Nombre. Ver 04 §3.1 y 09 §5.read/get/getRecord/page-response) con mensaje por-operación. Ver 04.language_id EN=1 / ES=2). Ver 06.Varios de los defectos más caros de esta lista no se manifestaban en pruebas porque el frontend consumía mocks: el JWT crudo, nombreCompleto en null, el status y el export 500 solo aparecieron al ejecutar la API real. De ahí la regla que cierra el patrón:
El build verde no es la entrega. Antes de declarar una feature terminada se ejecutan todos los endpoints y los flujos completos end-to-end contra el ambiente dev, verificando el contrato real que recibe el front: que
statusinterno nunca se exponga, que el estado de negocio sí, que losbyte[]bajen como binario, que los campos compuestos lleguen poblados y que toda respuesta sea tenant-scoped.
Cualquier hueco de cobertura o de validación pendiente se reporta; no se da por hecho. Ver 05.
| # | Antipatrón | Regla canónica |
|---|---|---|
| AP-1.1 | Confiar en @Filter de Hibernate |
Query nativa con tenant del token |
| AP-1.2 | Entidad de negocio global | SequenceBusinessBaseEntity + UK por tenant |
| AP-1.3 | Lecturas heredadas sin tenant | Variantes *AndTenant en lightloader/export |
| AP-1.4 | Scheduler sin tenant | TenantIterationSupport por iteración |
| AP-1.5 | Paginar con el default de commons (no aísla tenant) | NativeTenantPaginator (SQL nativo con tenant) |
| AP-2.1 | status como negocio |
Estado propio por entidad; status interno |
| AP-3.1 | Config JPA desviada | Patrón JPA 1:1 con accounting-core |
| AP-3.2 | Id asignado a mano | @SequenceGenerator (Hibernate genera) |
| AP-3.3 | Header Bearer crudo a SimappeJwt |
extractToken() siempre |
| AP-3.4 | Vista sin GRANT SELECT |
GRANT como parte del DDL de la vista |
| AP-4.1 | Descarga envuelta en SuccessResponse |
@IgnoreResponseBinding |
| AP-4.2 | Campo compuesto en null | Componer en el consumidor con fallback |
| AP-4.3 | Lectura desde entidad transaccional | Vista @Immutable + PageResponseSupport |
| AP-4.4 | UK/invariante mal modelado | Índice condicional + invariante separado |
| AP-4.5 | Vencimiento solo por estado | Validar TTL contra el reloj |
| AP-4.6 | Aplanado no aditivo / campo ≠ columna (filtro roto, ORA-01722) | Campo crudo = columna (filtrable) + *Nombre aditivo |
| AP-5.1 | Entregar sin pruebas/validación | JaCoCo > 80% + validación en vivo |
| AP-5.2 | Auditoría parcial | Auditar toda operación, mensaje por-operación |
| AP-5.3 | i18n incompleto | Siempre ES + EN |
11_ANTIPATRONES_DETECTADOS.md — Patrón canónico de feature backend — Nebula ERP / Centrica