Durante décadas se asumió que la arquitectura era algo que se decidía al principio y luego costaba muchísimo cambiar: "lo importante es acertar desde el primer día". La realidad es que ningún diseño sobrevive intacto al contacto con la evolución de los requisitos. La arquitectura evolutiva, formulada por Neal Ford, Rebecca Parsons y Patrick Kua, propone lo contrario: en lugar de intentar predecir el futuro, construimos sistemas que soportan el cambio guiado e incremental a través de múltiples dimensiones. ¿Cómo evitamos que, al evolucionar, la arquitectura se degrade sin que nadie se dé cuenta? Con fitness functions (funciones de aptitud): mecanismos objetivos y automatizados que miden si una característica arquitectónica se mantiene dentro de los límites aceptables. En esta lección veremos qué es el cambio incremental guiado, los tipos de fitness functions (atómicas, holísticas, activadas, continuas), ejemplos reales de tests de arquitectura con ArchUnit y cómo automatizar todo esto en el pipeline.

Contenido

  1. Qué es la arquitectura evolutiva
  2. Cambio incremental guiado
  3. Qué es una fitness function
  4. Tipos de fitness functions
  5. Tests de arquitectura con ArchUnit
  6. Otras fitness functions: rendimiento, acoplamiento, seguridad
  7. Automatización en el pipeline
  8. Errores Comunes y Consejos
  9. Ejercicios
  10. Conclusión

  1. Qué es la arquitectura evolutiva

Una arquitectura evolutiva es aquella que soporta el cambio guiado e incremental como un principio de primer nivel, a lo largo de múltiples dimensiones. Desglosemos esa definición:

  • Cambio guiado: no cualquier cambio, sino cambio en la dirección correcta. Necesitamos algo que nos diga si vamos por buen camino: ahí entran las fitness functions.
  • Incremental: la arquitectura cambia en pasos pequeños y reversibles, igual que vimos con el patrón Strangler Fig, no en saltos enormes.
  • Múltiples dimensiones: no solo el código. También rendimiento, seguridad, escalabilidad, accesibilidad... Cada característica importante es una dimensión que vigilar.

La premisa de fondo es humilde: no podemos predecir el futuro, así que en lugar de sobre-diseñar para requisitos imaginarios, diseñamos para poder cambiar y montamos guardarraíles que nos avisen si la arquitectura se desvía.

  1. Cambio incremental guiado

"Incremental" y "guiado" son dos propiedades separadas que se necesitan mutuamente:

  • Sin incrementalidad, cada cambio es grande y arriesgado: necesitas despliegues sin interrupción, automatización de pruebas y pipelines fiables para mover el sistema en pasos pequeños.
  • Sin guía, el cambio incremental puede llevarte despacio pero seguro... hacia el desastre. La guía la dan las fitness functions: comprueban que cada incremento mantiene las características arquitectónicas deseadas.

Una analogía útil: las fitness functions son a la arquitectura lo que los tests unitarios al código. Los tests no garantizan que el código sea bueno, pero impiden que se rompa lo que ya funcionaba. Las fitness functions no garantizan una arquitectura perfecta, pero impiden que se degrade en silencio mientras evoluciona.

  1. Qué es una fitness function

El término viene de los algoritmos genéticos, donde una fitness function mide cuán cerca está una solución del objetivo. En arquitectura, una fitness function es cualquier mecanismo que proporciona una evaluación objetiva de que una o varias características arquitectónicas se mantienen dentro de límites aceptables. La palabra clave es objetiva: debe dar un resultado medible, no una opinión.

Ejemplos de características y su fitness function:

Característica arquitectónica Fitness function (ejemplo)
Modularidad Test que prohíbe dependencias cíclicas entre paquetes
Rendimiento El percentil 95 de latencia se mantiene < 200 ms
Acoplamiento El acoplamiento aferente de un módulo no supera N
Seguridad Ninguna dependencia con CVE crítica conocida
Mantenibilidad La capa de dominio no importa frameworks de infraestructura

Lo importante es que cada una se puede medir y automatizar. Si no puedes escribir una comprobación que devuelva pasa/falla (o un número comparable con un umbral), no es una fitness function, es un deseo.

  1. Tipos de fitness functions

Las fitness functions se clasifican en varios ejes. Los más útiles:

Eje Tipos Significado
Alcance Atómica vs Holística Atómica: mide una sola característica de forma aislada. Holística: mide la interacción de varias a la vez (p. ej. seguridad bajo carga).
Ejecución Activada (triggered) vs Continua Activada: corre por un evento (un commit, un despliegue). Continua: monitoriza en producción en tiempo real.
Métrica Estática vs Dinámica Estática: resultado fijo (pasa/falla, como un test). Dinámica: depende del contexto (un umbral que varía con la carga).
Automatización Automatizada vs Manual La inmensa mayoría deben ser automatizadas; las manuales (revisiones) se reservan para lo que no se puede medir.

Las más comunes y baratas de implementar son las atómicas, activadas y automatizadas: tests que corren en cada build y comprueban una regla concreta. Los tests de arquitectura con ArchUnit caen justo aquí, y son el mejor punto de partida.

  1. Tests de arquitectura con ArchUnit

ArchUnit es una librería Java que permite escribir reglas de arquitectura como tests normales (JUnit). Analiza el bytecode de tus clases y comprueba afirmaciones sobre paquetes, dependencias, nombres, capas, etc. Es la forma más directa de convertir un ADR en una fitness function ejecutable.

import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.domain.JavaClasses;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;

class ReglasArquitecturaTest {

    // Importamos todas las clases del proyecto una sola vez
    private final JavaClasses clases =
        new ClassFileImporter().importPackages("com.fiatc.tienda");

    @Test
    void el_dominio_no_depende_de_la_infraestructura() {
        noClasses()
            .that().resideInAPackage("..dominio..")
            .should().dependOnClassesThat().resideInAPackage("..infraestructura..")
            .check(clases);
    }
}

Analicemos este test, que materializa una regla clave de la arquitectura hexagonal:

  • importPackages("com.fiatc.tienda") carga el bytecode de todas las clases bajo ese paquete. ArchUnit no ejecuta tu código; lo inspecciona estáticamente.
  • noClasses().that().resideInAPackage("..dominio..") selecciona las clases del dominio. El .. es un comodín de ArchUnit: "cualquier paquete que contenga dominio a cualquier nivel".
  • .should().dependOnClassesThat().resideInAPackage("..infraestructura..") define lo prohibido: depender de la infraestructura.
  • .check(clases) ejecuta la regla y falla el test si alguna clase del dominio importa algo de infraestructura.

Si mañana alguien añade en una clase de dominio un import de Spring o de un repositorio JPA, este test se pondrá rojo en el pipeline. La regla arquitectónica deja de depender de la disciplina humana.

@Test
void los_servicios_de_aplicacion_se_llaman_correctamente() {
    classes()
        .that().resideInAPackage("..aplicacion..")
        .and().areAnnotatedWith(Service.class)
        .should().haveSimpleNameEndingWith("ServicioAplicacion")
        .check(clases);
}

@Test
void no_debe_haber_dependencias_ciclicas_entre_modulos() {
    slices()
        .matching("com.fiatc.tienda.(*)..")
        .should().beFreeOfCycles()
        .check(clases);
}

Estos dos tests añaden más guardarraíles:

  • El primero impone una convención de nombres: toda clase @Service en la capa de aplicación debe terminar en ServicioAplicacion. Parece menor, pero la consistencia de nombres es una característica de mantenibilidad real.
  • El segundo es de los más valiosos: slices() divide el sistema en "rodajas" por el primer subpaquete (Pedidos, Catálogo, Pagos...) y beFreeOfCycles() comprueba que no haya dependencias cíclicas entre ellas. Los ciclos son el principio de la "bola de barro"; detectarlos automáticamente protege la modularidad.

ArchUnit incluso ofrece una API de alto nivel para capas:

@Test
void se_respetan_las_capas() {
    layeredArchitecture().consideringAllDependencies()
        .layer("Presentacion").definedBy("..presentacion..")
        .layer("Aplicacion").definedBy("..aplicacion..")
        .layer("Dominio").definedBy("..dominio..")
        // El dominio no puede ser accedido por nadie de fuera salvo Aplicacion
        .whereLayer("Dominio").mayOnlyBeAccessedByLayers("Aplicacion")
        .whereLayer("Presentacion").mayNotBeAccessedByAnyLayer()
        .check(clases);
}

Aquí declaramos las capas y sus reglas de acceso: la presentación no puede ser accedida por nadie (está en la cima) y el dominio solo es accesible desde aplicación. ArchUnit verifica que las dependencias reales del código respeten esa jerarquía. Es un ADR sobre capas convertido en una prueba que vela por sí misma.

  1. Otras fitness functions: rendimiento, acoplamiento, seguridad

No todo se mide con ArchUnit. Otras dimensiones necesitan otras herramientas:

  • Rendimiento (holística, continua): un test de carga (Gatling, k6) que falla si el p95 de latencia supera un umbral. Puede correr en CI (activada) o monitorizarse en producción (continua).
  • Acoplamiento (atómica, activada): métricas de acoplamiento aferente/eferente con herramientas como JDepend o el propio ArchUnit, comparadas contra un máximo.
  • Seguridad (atómica, activada): un escáner de dependencias (OWASP Dependency-Check, Trivy) que falla el build si aparece una CVE crítica.
  • Cobertura/tamaño (atómica): reglas que impiden que un módulo crezca por encima de cierto número de clases sin revisión.
# Fitness function de seguridad en el pipeline: escaneo de dependencias
escaneo-dependencias:
  stage: verificacion
  script:
    - trivy fs --severity CRITICAL --exit-code 1 .
  # exit-code 1 hace que el job (y el build) falle si hay CVE críticas

Este job convierte la característica "el sistema no usa dependencias con vulnerabilidades críticas conocidas" en una fitness function automatizada: trivy escanea las dependencias y, con --exit-code 1, hace fallar el build si encuentra una CVE crítica. La seguridad deja de ser una auditoría puntual y pasa a verificarse en cada cambio.

  1. Automatización en el pipeline

Una fitness function que hay que ejecutar a mano se acabará olvidando. El valor real aparece cuando viven en el pipeline de CI/CD y fallan el build automáticamente.

graph LR
    Commit[Commit / PR] --> Build[Compilar]
    Build --> Unit[Tests unitarios]
    Unit --> Arch[Fitness: ArchUnit]
    Arch --> Sec[Fitness: seguridad]
    Sec --> Perf[Fitness: rendimiento]
    Perf -->|todo verde| Deploy[Desplegar]
    Arch -->|rojo| Stop[Build falla]

El diagrama muestra las fitness functions como etapas del pipeline, al mismo nivel que los tests unitarios. Si la regla de arquitectura (ArchUnit) falla, el build se detiene y el cambio no llega a producción. Recomendaciones para que esto funcione:

  • Rápidas primero. Pon las fitness atómicas y baratas (ArchUnit, linters) al principio; las caras (carga) al final o en paralelo.
  • Mensajes claros. Cuando una regla falle, el error debe explicar qué regla y por qué, no solo "test rojo".
  • Versionadas con el código. Las reglas viven en el repo y evolucionan en pull requests, igual que los ADR.
  • Pocas y significativas al principio. Empieza con 3-4 reglas que de verdad importen; añade más a medida que duelan.

  1. Errores Comunes y Consejos

  • Escribir fitness functions subjetivas. "El código debe ser legible" no es una fitness function. Si no devuelve pasa/falla o un número, no sirve.
  • Demasiadas reglas de golpe. Un proyecto que añade 50 reglas de ArchUnit el primer día acaba con builds rojos por todas partes y la gente desactivándolas. Empieza con pocas.
  • Reglas que no fallan el build. Una fitness function que solo genera un informe que nadie lee es decorativa. Debe poder detener el pipeline.
  • Confundir fitness functions con tests unitarios. Los unitarios prueban comportamiento; las fitness functions prueban características arquitectónicas (estructura, rendimiento, seguridad).
  • No mantenerlas. Cuando una decisión arquitectónica cambia (nuevo ADR), las reglas asociadas deben actualizarse. Una regla obsoleta que falla sin razón erosiona la confianza.
  • Consejo: empareja cada ADR importante con una fitness function que lo verifique. Así la decisión documentada y la decisión real no se separan nunca.

  1. Ejercicios

Ejercicio 1. Clasifica estas fitness functions según alcance (atómica/holística) y ejecución (activada/continua): (a) un test de ArchUnit en CI que prohíbe ciclos; (b) un monitor en producción que alerta si el p95 supera 300 ms; (c) un test de carga que mide latencia mientras corre un escaneo de seguridad.

Ejercicio 2. Escribe (en pseudocódigo o con la API de ArchUnit) una regla que prohíba que cualquier clase de la capa de presentación (..presentacion..) acceda directamente a la capa de persistencia (..persistencia..), saltándose la capa de aplicación.

Ejercicio 3. Tu organización ha aceptado un ADR que dice "ninguna dependencia con CVE crítica en producción". ¿Cómo lo convertirías en una fitness function automatizada y en qué etapa del pipeline la pondrías?

Soluciones

Solución 1. (a) Atómica y activada (mide una característica —modularidad— y corre por un commit). (b) Atómica y continua (mide rendimiento de forma permanente en producción). (c) Holística y activada (mide la interacción de rendimiento y seguridad a la vez, disparada por el pipeline).

Solución 2.

noClasses()
    .that().resideInAPackage("..presentacion..")
    .should().dependOnClassesThat().resideInAPackage("..persistencia..")
    .check(clases);

La presentación debe pasar siempre por aplicación; esta regla falla el build si una clase de presentación importa directamente algo de persistencia.

Solución 3. Con un escáner de dependencias (Trivy, OWASP Dependency-Check) ejecutado en el pipeline con un umbral de severidad crítica y --exit-code 1, de modo que el build falle si aparece una CVE crítica. Va en una etapa de verificación, antes del despliegue, para impedir que el artefacto vulnerable llegue a producción.

  1. Conclusión

La arquitectura no es una foto fija que se decide al inicio, sino algo vivo que evoluciona. La arquitectura evolutiva abraza esa realidad apoyándose en el cambio incremental guiado, y las fitness functions son la brújula que mantiene la dirección: comprobaciones objetivas y automatizadas que impiden que las características arquitectónicas se degraden en silencio. Hemos visto cómo clasificarlas (atómicas/holísticas, activadas/continuas), cómo escribir tests de arquitectura reales con ArchUnit para proteger capas, nombres y ciclos, cómo cubrir otras dimensiones como rendimiento y seguridad, y cómo integrarlo todo en el pipeline para que la arquitectura se defienda sola. Con esto cerramos el bloque conceptual del módulo. En la última lección del curso pondremos en práctica todo lo aprendido —estilos, datos, despliegue, gobernanza y evolución— en un estudio de caso de extremo a extremo: el diseño completo de una plataforma de comercio electrónico.

Curso de Arquitectura de Aplicaciones

Módulo 1: Fundamentos de la Arquitectura de Aplicaciones

Módulo 2: Principios y Tácticas de Diseño

Módulo 3: Estilos y Patrones Arquitectónicos

Módulo 4: Arquitecturas Distribuidas y Microservicios

Módulo 5: Arquitecturas Dirigidas por Eventos y Mensajería

Módulo 6: Diseño Dirigido por el Dominio (DDD)

Módulo 7: Datos y Persistencia

Módulo 8: Arquitectura en la Nube y Despliegue

Módulo 9: Calidad, Seguridad y Observabilidad

Módulo 10: Evolución, Gobernanza y Casos Prácticos

© Copyright 2026. Todos los derechos reservados