Las funciones de adecuación permiten validar que una arquitectura de software cumple con las decisiones, restricciones y objetivos que el equipo definió. Piensa en ellas como pruebas automáticas para tu arquitectura: en lugar de comprobar que una función retorna el valor correcto, verifican que el sistema respete ciertas reglas estructurales, de rendimiento, seguridad o acoplamiento.
El concepto viene de la arquitectura evolutiva, donde se busca que el sistema pueda cambiar de forma segura a lo largo del tiempo. Si no hay mecanismos que controlen esos cambios, la arquitectura se desgasta: aparecen dependencias inesperadas, módulos que no deberían comunicarse entre sí, tiempos de respuesta que empeoran silenciosamente, y deuda técnica que nadie detecta hasta que ya es cara de arreglar.
Al terminar esta guía podrás:
- Redactar una decisión arquitectónica como una condición medible.
- Elegir si la verificación debe bloquear, alertar o solo informar.
- Integrar funciones de adecuación en CI sin paralizar al equipo.
- Evitar métricas decorativas y reglas que generan falsos positivos.
De una intención a una regla ejecutable
“Queremos una arquitectura limpia” no se puede verificar. Una función de adecuación necesita cinco elementos concretos:
| Elemento | Pregunta | Ejemplo |
|---|---|---|
| Decisión | ¿Qué queremos proteger? | Dominio independiente de infraestructura |
| Señal | ¿Qué podemos observar? | Imports desde domain hacia infrastructure |
| Umbral | ¿Cuándo incumple? | Más de cero dependencias |
| Acción | ¿Qué ocurre al incumplir? | Bloquear el pull request |
| Revisión | ¿Cuándo validamos que sigue teniendo sentido? | Cada trimestre |
Una plantilla útil es: “Dado [ámbito], medimos [señal]; debe mantenerse [umbral], de lo contrario [acción]”.
Por ejemplo: “En el módulo de pagos, medimos dependencias desde dominio hacia infraestructura; deben ser cero, de lo contrario el pipeline bloquea el merge”.
¿Por qué las funciones de adecuación importan?
En muchos equipos, las decisiones de arquitectura viven en documentos, diagramas o reuniones. Con el tiempo, es fácil que el código real se desvíe de esas decisiones. Las funciones de adecuación cierran esa brecha al convertir reglas arquitectónicas en verificaciones ejecutables.
Algunos beneficios claros:
- Detectan desviaciones temprano, antes de que se conviertan en problemas grandes.
- Documentan las reglas arquitectónicas de forma viva, dentro del repositorio.
- Protegen inversiones técnicas, como separar dominios, mantener la independencia de capas o respetar límites de dependencias.
- Facilitan refactorizaciones porque el equipo puede cambiar código con confianza.
- Son automáticas, por lo que no dependen de revisiones manuales ni de la memoria de una persona.
¿Qué tipos de funciones de adecuación existen?
No todas las funciones de adecuación se ven igual. Se pueden clasificar según lo que evalúan y cómo lo hacen.
Según el ámbito
- Estructurales: verifican la organización del código. Por ejemplo, que la capa de dominio no dependa de infraestructura, o que ciertos paquetes no se importen entre sí.
- De rendimiento: comprueban tiempos de respuesta, uso de memoria, consumo de CPU o latencia bajo carga.
- De seguridad: detectan dependencias vulnerables, configuraciones inseguras o exposición de datos sensibles.
- De observabilidad: validan que existan métricas, logs o trazas en puntos críticos.
- De calidad técnica: miden deuda técnica, duplicación de código, complejidad ciclomática o cobertura de pruebas.
- De interoperabilidad: aseguran que los contratos entre servicios se respeten, por ejemplo mediante pruebas de contrato.
Según la automatización
- Automáticas: se ejecutan en el pipeline de CI/CD sin intervención humana.
- Semiautomáticas: generan reportes o alertas que una persona debe revisar y decidir.
- Manuales: se realizan en auditorías o revisiones periódicas, aunque cuentan con checklists o herramientas.
Según el momento de ejecución
- Continuas: corren en cada cambio, por ejemplo en cada push o pull request.
- Periódicas: se ejecutan en intervalos definidos, como una vez al día o por sprint.
- Puntuales: se ejecutan ante eventos específicos, como antes de un despliegue o una liberación.
¿Cómo se implementan?
Implementar funciones de adecuación no requiere una herramienta única ni un cambio radical. Se trata de identificar qué decisiones arquitectónicas quieres proteger y luego elegir la forma más sencilla de verificarlas.
El flujo suele ser este:
flowchart LR
A[Definir decisión arquitectónica] --> B[Elegir indicador medible]
B --> C[Escribir función de adecuación]
C --> D[Integrar en CI/CD]
D --> E[Fallar o alertar ante desviaciones]
Paso 1: identificar decisiones importantes
Empieza por las reglas que realmente importan para tu proyecto. No intentes verificarlo todo desde el primer día. Algunas preguntas útiles:
- ¿Qué capas o módulos deben mantenerse separados?
- ¿Qué dependencias están prohibidas?
- ¿Cuáles son los límites de rendimiento aceptables?
- ¿Qué tan alta puede ser la complejidad de un método o clase?
- ¿Hay librerías o patrones que no queremos usar?
Paso 2: elegir una herramienta adecuada
Dependiendo del tipo de función, existen diferentes opciones:
| Tipo | Ejemplos de herramientas |
|---|---|
| Estructural | ArchUnit, NetArchTest, Dependency-Check, JQAssistant |
| Rendimiento | k6, JMeter, Gatling, Lighthouse |
| Seguridad | OWASP Dependency-Check, Snyk, Trivy, SonarQube |
| Calidad técnica | SonarQube, Code Climate, ESLint, RuboCop |
| Contratos | Pact, Spring Cloud Contract |
También puedes escribir tus propias pruebas con el framework de testing que ya uses. A veces una simple prueba unitaria que inspeccione ensamblados o clases es suficiente.
Paso 3: integrar en el flujo de trabajo
La clave está en que las funciones de adecuación se ejecuten de forma habitual. Lo ideal es incluirlas en el pipeline de integración continua, de modo que un cambio que rompa una regla arquitectónica no pueda avanzar.
flowchart TB
subgraph Pipeline["Pipeline de CI/CD"]
direction TB
Build["Compilación"] --> Test["Pruebas unitarias"]
Test --> Fitness["Funciones de adecuación"]
Fitness --> Security["Escaneo de seguridad"]
Security --> Deploy["Despliegue"]
end
Fitness -->|falla| Block["Bloquear merge"]
Fitness -->|pasa| Deploy
No es necesario que todas sean bloqueantes desde el inicio. Puedes comenzar con advertencias y, una vez estabilizadas, convertirlas en errores que impidan integrar el cambio.
Ejemplos prácticos
Ejemplo 1: evitar dependencias entre capas
Supón que decides que la capa de dominio no debe depender de infraestructura. Con una herramienta como ArchUnit en Java podrías escribir algo como esto:
@ArchTest
static final ArchRule domainShouldNotDependOnInfrastructure =
noClasses()
.that()
.resideInAPackage("..domain..")
.should()
.dependOnClassesThat()
.resideInAPackage("..infrastructure..");
Si alguien introduce accidentalmente una dependencia prohibida, el pipeline falla y el equipo lo corrige antes de que llegue a producción.
En un proyecto TypeScript puedes empezar sin una plataforma compleja. ESLint permite prohibir imports concretos dentro de la capa de dominio:
// eslint.config.mjs
export default [
{
files: ['src/**/domain/**/*.ts'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['**/infrastructure/**', '**/http/**'],
message: 'El dominio no puede depender de adaptadores externos.',
},
],
},
],
},
},
];
La regla es pequeña, rápida y devuelve el error en el editor antes de llegar a CI. Si luego necesitas validar grafos completos, ciclos o límites entre muchos paquetes, puedes adoptar una herramienta especializada sin cambiar la decisión que estás protegiendo.
Ejemplo 2: límites de rendimiento
Una API que responde en más de 500 milisegundos bajo carga normal puede degradar la experiencia del usuario. Una función de adecuación con k6 podría lucir así:
import http from "k6/http";
import { check } from "k6";
export const options = {
thresholds: {
http_req_duration: ["p(95)<500"],
},
};
export default function () {
const res = http.get("https://api.ejemplo.com/productos");
check(res, { "status es 200": (r) => r.status === 200 });
}
El test no solo mide el rendimiento: lo convierte en una condición que debe cumplirse.
Ejemplo 3: dependencias sin vulnerabilidades conocidas
En un proyecto con muchas librerías de terceros, es fácil que una dependencia desactualizada introduzca riesgos de seguridad. Una función de adecuación puede verificar que no existan vulnerabilidades críticas conocidas:
snyk test --severity-threshold=high
Si aparece una vulnerabilidad alta, el pipeline se detiene hasta que se actualice o se mitigue.
Ejemplo 4: complejidad ciclomática máxima
Una alta complejidad ciclomática suele indicar código difícil de mantener y probar. Con herramientas como SonarQube o linters puedes definir umbrales:
# Configuración de ejemplo para ESLint
rules:
complexity: ["error", 10]
Esto obliga al equipo a refactorizar funciones demasiado complejas antes de que se acumulen.
Un enfoque gradual
No hace falta implementar todas las funciones de adecuación al mismo tiempo. Lo recomendable es empezar pequeño y crecer según las necesidades del proyecto.
flowchart LR
Start["Empieza con una regla clara"] --> Auto["Automatízala"]
Auto --> CI["Intégrala en CI/CD"]
CI --> Evolve["Añade más reglas con el tiempo"]
Evolve --> Start
Comienza por la decisión arquitectónica que más dolor te cause o que más se ignore actualmente. Automatiza una sola regla, estabilízala y luego repite el proceso. Así construyes una red de seguridad arquitectónica sin saturar al equipo.
Define niveles de respuesta
No toda desviación debe detener un despliegue. Clasificar la respuesta reduce fricción y evita que el equipo desactive controles útiles:
| Nivel | Uso recomendado | Ejemplo |
|---|---|---|
| Informativo | Crear una línea base | Complejidad actual por módulo |
| Advertencia | Tendencia que requiere seguimiento | Cobertura bajó 2 % |
| Bloqueante | Riesgo inmediato y corrección clara | Dominio importa infraestructura |
| Presupuesto | Degradación acumulativa | El bundle no debe crecer más de 20 KB |
Empieza en modo informativo cuando no conoces la distribución real de la métrica. Convierte una regla en bloqueante cuando el umbral sea estable, el mensaje indique cómo corregirla y las excepciones tengan un proceso explícito.
Evita reglas que dañan al equipo
Una función de adecuación también necesita mantenimiento. Revísala si presenta alguna de estas señales:
- Falla con frecuencia sin representar un riesgo real.
- Incentiva trucos para “pasar el check” en vez de mejorar el sistema.
- Mide un proxy que ya no se relaciona con el objetivo original.
- No tiene una persona o equipo responsable.
- Su corrección cuesta más que el riesgo que reduce.
- Conserva un umbral arbitrario que nadie puede justificar.
Registra las excepciones con motivo y fecha de expiración. Una lista permanente de archivos ignorados suele ser deuda arquitectónica escondida.
Plan de adopción en cuatro iteraciones
- Inventario: selecciona tres decisiones críticas y define su señal observable.
- Línea base: ejecuta las verificaciones sin bloquear durante una o dos semanas.
- Estabilización: corrige falsos positivos, documenta excepciones y asigna responsables.
- Protección: bloquea únicamente las reglas maduras y revisa su utilidad periódicamente.
El resultado debe ser una red de seguridad pequeña y confiable, no una colección de herramientas que nadie entiende.
¿Qué no son las funciones de adecuación?
Es importante no confundirlas con otras prácticas:
- No son un sustituto de las pruebas unitarias. Complementan, no reemplazan.
- No son documentación estática. Son verificaciones vivas que se ejecutan.
- No deben convertirse en un obstáculo burocrático. Si una regla no aporta valor, revísala o elimínala.
- No son solo para arquitecturas grandes. Incluso en aplicaciones pequeñas, una regla de acoplamiento o rendimiento puede evitar muchos problemas.
Conclusión
Las funciones de adecuación ayudan a que la arquitectura de software no se quede en el papel. Permiten detectar desviaciones de forma temprana, automatizar reglas importantes y dar confianza al equipo para evolucionar el sistema.
No se trata de perseguir la perfección arquitectónica, sino de proteger conscientemente las decisiones que el equipo considera valiosas. Con una herramienta adecuada, una regla clara y un lugar en el pipeline de integración continua, cualquier proyecto puede empezar a beneficiarse de ellas.
Checklist para tu primera función de adecuación
- La decisión que protege está escrita en una frase.
- La señal puede medirse de manera repetible.
- El umbral se basa en riesgo o en una línea base conocida.
- El mensaje explica qué falló y cómo actuar.
- La ejecución ocurre lo más cerca posible del cambio.
- Existe un responsable y una fecha de revisión.
- Las excepciones quedan documentadas y expiran.