En pocas palabras
Mecanismos objetivos (pruebas, métricas, reglas) que evalúan si el sistema sigue cumpliendo una característica de arquitectura deseada a medida que evoluciona.
Qué problema resuelve
Decisiones de arquitectura que se erosionan en silencio: dependencias prohibidas, latencias que crecen, ciclos entre módulos.
Cuándo utilizarlo
Sistemas que evolucionan con muchos equipos. Ejemplo: impedir que el módulo de dominio dependa de la capa web o que una API supere 300 ms de p95.
De la idea a la práctica
Funcionamiento, ejemplos y decisiones para pasar al siguiente nivel.
Tipos
| Dimensión | Opciones |
|---|---|
| Alcance | Atómica (un aspecto aislado) u holística (varios combinados) |
| Momento | Disparada (en CI, al hacer merge) o continua (en producción) |
| Resultado | Estática (pasa/no pasa) o dinámica (umbral que depende del contexto) |
| Automatización | Automática o manual (p. ej. revisión legal), pero explícita |
Ejemplos por característica
| Característica | Fitness function |
|---|---|
| Modularidad | ArchUnit: el dominio no depende de infraestructura; sin ciclos |
| Rendimiento | Prueba de carga en CI: p95 < 300 ms con 200 RPS |
| Seguridad | Ninguna dependencia con CVE crítico (SBOM + escáner) |
| Disponibilidad | SLO de 99,9 % con alerta por consumo de error budget |
| Coste | Coste por transacción no supera el umbral mensual |
| Evolución de API | Pruebas de contrato que detectan cambios incompatibles (Pact) |
Ejemplo con ArchUnit
@AnalyzeClasses(packages = "com.krediya.originacion")
class ArquitecturaTest {
@ArchTest
static final ArchRule capas = layeredArchitecture()
.consideringAllDependencies()
.layer("Web").definedBy("..web..")
.layer("Aplicacion").definedBy("..aplicacion..")
.layer("Dominio").definedBy("..dominio..")
.layer("Infraestructura").definedBy("..infraestructura..")
.whereLayer("Web").mayNotBeAccessedByAnyLayer()
.whereLayer("Aplicacion").mayOnlyBeAccessedByLayers("Web")
.whereLayer("Dominio").mayOnlyBeAccessedByLayers("Aplicacion", "Infraestructura");
@ArchTest
static final ArchRule sinCiclos = slices()
.matching("com.krediya.originacion.(*)..")
.should().beFreeOfCycles();
}
Se ejecuta como una prueba más con JUnit 5; si alguien importa un repositorio JPA desde un controlador, el build falla con un mensaje explícito. Equivalentes: dependency-cruiser (JavaScript/TypeScript), import-linter (Python), NetArchTest (.NET).
Antipatrones frecuentes
- Reglas que fallan siempre y se acaban ignorando o desactivando.
- Medir lo fácil (cobertura) en lugar de lo importante (acoplamiento, latencia).
- Fitness functions sin dueño: cuando falla una, nadie sabe si cambiar el código o la regla.
Equivalente en TypeScript con dependency-cruiser
// .dependency-cruiser.cjs
module.exports = {
forbidden: [
{
name: 'dominio-sin-infraestructura',
severity: 'error',
from: { path: '^src/dominio' },
to: { path: '^src/(infraestructura|web)' },
},
{ name: 'sin-ciclos', severity: 'error', from: {}, to: { circular: true } },
{
name: 'apps-no-importan-otras-apps',
severity: 'error',
from: { path: '^apps/([^/]+)/' },
to: { path: '^apps/', pathNot: '^apps/$1/' },
},
],
};
npx depcruise src --config .dependency-cruiser.cjs
Fitness functions en producción
| Característica | Función continua |
|---|---|
| Disponibilidad | Alertas de burn rate sobre el SLO |
| Latencia | Monitorización sintética de los recorridos críticos cada minuto |
| Resiliencia | Experimentos de caos programados con resultado esperado |
| Seguridad | Escaneo continuo de configuración de nube (CSPM) |
| Coste | Alerta si el coste por transacción supera el umbral |
| Cumplimiento | Políticas como código evaluadas sobre el estado real (continuous compliance) |
Catálogo de fitness functions
| Nombre | Característica | Tipo | Umbral | Dónde | Dueño |
|---|---|---|---|---|---|
| capas-dominio | Modularidad | Atómica, disparada | 0 violaciones | PR | Equipo |
| p95-api-solicitudes | Rendimiento | Atómica, disparada | < 300 ms a 200 RPS | Nocturno | Equipo |
| cve-criticos | Seguridad | Atómica, disparada | 0 críticos sin VEX | PR y diario | Seguridad |
| slo-disponibilidad | Disponibilidad | Holística, continua | 99,9 % en 28 días | Producción | Equipo + SRE |
| coste-por-solicitud | Coste | Holística, continua | < 0,02 USD | Mensual | Plataforma |
Un catálogo visible ayuda a la gerencia a ver qué decisiones de arquitectura están protegidas y cuáles dependen solo de la buena voluntad.
Introducir una regla en un código existente
- Ejecuta la regla y registra las violaciones actuales como línea base (ArchUnit ofrece freezing de violaciones).
- Haz fallar el build solo con violaciones nuevas.
- Reduce la línea base de forma incremental en cada iteración.
Criterios de implementación
Deriva cada función de un atributo de calidad priorizado y enlázala desde el ADR que la motivó. Ejecuta las atómicas en cada pull request y las holísticas en un pipeline nocturno o en producción. Revisa los umbrales cuando cambie el contexto, dejando constancia de por qué.
Continúa en las fuentes
Documentación oficial para verificar detalles y explorar más.